o2 dev
108 subscribers
49 photos
4 videos
25 files
54 links
About o2 engine development
加入频道
Далее, были изощрения с самим классом умного указателя. Как я в начале писал, у меня используется внешний счетчик, но в одном блоке памяти с объектом. То есть аллоцируя объект, выделяется чуть больше памяти и перед объектом кладется счетчик. Это улучшает работу с кешем процессора, т.к. счетчик обращаясь к объекту скорее всего будет взаимодействие со счетчиком. При обращении к счетчику мы сразу возьмем весь объект или его кусок в кеш, и его не нужно ждать из памяти, как в случае если счетчик и объект не рядом.

Однако у подхода есть и минус: если остались только слабые ссылки на объект, то после вызова деструктора объекта его память не освобождается, а весь кусок памяти со счетчиком висит пока есть хотя бы одна слабая ссылка. Память освобождается только когда и сильных, и слабых ссылок не остается. В случае с отдельным счетчиком такой проблемы нет, память освобождается, в памяти висит только сам счетчик.

Работает это все через функцию-конструктор, вместо обычного вызова new. То есть создание объекта получается вот так:
auto myObject = mmake<MyClass>(...);.

Такое оборачивание нужно для того, чтобы перегрузить поведение выделения участка памяти и добавить инициализацию счетчика:
- аллоцируем участок памяти размером sizeof(RefCounter) + sizeof(MyClass)
- вызываем placement new для счетчика
- вызываем placement new для объекта
- передать счетчик ссылок в объект

placement new - это конструирование объекта, наподобие обычному new, но без аллокации, а с уже указанным участком памяти

Здесь сразу появляется пачка проблем. Начиная с банального удобства, ведь когда пишешь mmake<MyClass> в IDE, она не знает что тебе нужно показать список аргументов конструктора, для него это просто какая-то шаблонная функция с вариативным кол-вом аргументов. Соответственно, нужно просто помнить список аргументов

Далее проблема использования сильных и слабых ссылок в конструкторе класса. Например, конструируя Actor, внутри создается так же и ActorTransform, который хочет держать слабую ссылку на сам Actor. Или в конструкторе копирования, нужно продублировать всех детей, которые так же хотят иметь слабую ссылку на родителя. В целом есть еще кейсы, когда в конструкторе объект уже хочет взять сильную или слабую ссылку на себя же. Но так сделать не получится, потому что счетчика еще нет у объекта

Это заставляет искать обходные пути, например писать фабрики для объектов. Для копирования делать функции а-ля Clone(), с раздельным конструированием и прокидыванием ссылок внутри. Я попробовал несколько альтернативных подходов

1. использовать фабрики, как описано выше. Просто не удобно, нужно знать о фабриках, получается плохой и не очевидный синтаксис
2. использовать методы пост-инициализации, в которых прокидывать ссылки в детей и т.п. Это нарушает концепцию RAII, которая гласит что объект готов к использованию сразу после конструктора
3. как-то хитро закинуть счетчик ссылок в конструктор, через вторичное глобальное хранилище. То есть объявляем глобальную переменную на тред, в которую кладем счетчик, откуда уже его можно взять в конструкторе. Убивает кеш процессора
4. закидывать счетчик ссылок прямо в конструктор параметром. Придется тащить в параметры конструктора сервисную переменную, а так же во всех наследников этого класса
К себе я взял 2 и 4 пункты. 1й может реализовать сам юзер. 3й вредный для производительности. Со 2м оказалось не удобно, т.к. конструктор разбивался фактически на две функции. Писать алгоритмы в таком ключе не просто, а тем более переписывать уже готовые конструкторы, где есть расчет что ссылку можно взять сразу (с сырыми указателями такой проблемы ведь не было). А вот с 4м получилось все более-менее хорошо: алгоритмы просты, т.к. можно сразу работать со ссылками на себя, но приходится везде протаскивать RefCounter* refCounter в параметре конструктора, в т.ч. по всех наследниках. Протаскивание довольно неприятно, ведь это по дефолту уходит в наследников компонент и акторов, но все-таки терпимо и делается быстро.

А вот с тем, как закинуть это в конструктор - отдельная магия. Для этого функция mmake<T> умеет понимать, принимает ли конструктор T() первым параметром RefCounter* refCounter. Если да, то созданный ранее счетчик передается первым параметром в конструктор. Далее идут параметры, переданные в mmake<>(...). Там же в mmake<>() делается проверка на наличие метода PostRefConstruct() в конструируемом типе, и если есть - вызывается.

Так же сама функция mmake<>() собственно не функция, а хитрый макрос, который умеет собирать место аллокации в сорцах для последующего анализа аллокаций. То есть создавая объект через mmake<>(), мы получаем запись о том что в таком-то .cpp в такой-то строке был создан объект размером Х байт. Далее можно просто смотреть откуда в сорцах аллоцируется больше всего. Работает это через прослойку с маленьким объектом на стеке, который запоминает путь к .cpp и строку через макросы __FILE и LINE__.

Собственно вот так выглядит комбайн по созданию умных указателей. Внутри он делает все что нужно, на выходе выдавая готовый Ref<> с вызовом всего что определено пользователем.
Отдельный пункт про наследование. Так уж выходит, что базовый класс для объектов со счетчиком - RefCounterable - начинает пересекаться при множественном наследовании.

Например, есть class ISceneDrawable: public RefCounterable
и есть class CursorEventsListener: public RefCounterable
и ты пытаешься сделать class MyClass: public ISceneDrawable, public CursorEventsListener.

Делать разные счетчики, очевидно не правильно. Можно попробовать применить виртуальное наследование, но тогда все становится хуже, причем весьма неочевидным способом. Дело в том что компилятор перестанет вообще вызывает конструктор RefCounterable(refCounter) у наследников, даже если это явно прописано, а будет вызывать просто RefCounterable(). Такая вот редиска при множественном виртуальном наследовании.. Чтобы вызывался RefCounterable(refCounter), нужно его вызывать из всех классов. Что не удобно, не красиво и легко забыть, получив очередную пулю в колено.

В общем, можно пойти двумя другими путями: использовать интерфейс объекта со счетчиком, убрав из него явно ссылку на счетчик, заменив его виртуальной функцией, которая его возвращает. Тогда с множественным наследованием все окей, но приходится перегружать эту функцию в классе наследнике. Другой путь, немного расточительный по памяти, иметь на каждый базовый класс ссылку на счетчик, и всем раздать одну и ту же ссылку при конструировании. Мы итак уже прокидываем его через конструктор, теперь просто нужно не забыть его прокинуть везде. Немного помогает шаблонно-макросная магия: достаточно внутри класса перечислить всех наследников.
Что ж, имея этот джентельменский набор удобств, остальной перевод указателей превращается практически в рутину. Это все выглядит довольно просто, но пока экспериментируешь и перебираешь подходы, путь оказывается длинным и тернистым. Однако познавательным. Все больше убеждаюсь что написание базовых примитивов самому, будь то контейнеры или умные указатели (или игровой движок), приходит гораздо более глубокое понимание уже готовых инструментов.

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

Теперь интересно будет вкрутить тот самый GC в умные указатели, с которого и проросла идея своих смарт-поинтеров. Это будет в виде ужасных макосов, вкоряченных в Ref<>, а так же потребуется как-то разметить "рутовые" ссылки, от которых начнется построение дерева памяти. Но эту штуку я уже давным-давно делал и даже писал статью на хабр.
Еще довольно быстро прикрутил к проекту профилировщик tracy. Он активно используется на работе в Playrix, и весьма удобен для геймдева. Обычно профайлеры показывают статистику за весь промежуток работы приложения, что для игр не удобно. В играх гораздо лучше рассматривать время кадра, статистику между кадрами. Отделять разные этапы загрузки игры. Tracy позволяет все это увидеть, разметить все как тебе нужно. А еще он подсоединяется по сети, и можно профайлить прямо с реального девайса. Это весьма удобно

Интеграция очень простая. Подключаешь сабмодулем, добавляешь пару сорцов или подключаешь cmake, делаешь минимальную разметку и готово. У меня ушло пару часов чтобы подключить tracy к проекту.

Профилировщик кадра - это та штука, которой мне не хватало на протяжении всей разработки движка. До этого выкручивался профайлером MSVS и своими простыми профайлерами, которые просто суммировали время работы функций. Но теперь можно детально изучать время кадра и загрузку. В целом, даже сейчас, цифры получаются приятные, учитывая объем сцены в редакторе. Ведь сам редактор сделано через движковый UI, что само по себе довольно неплохой тест движка. Много функционала, графики, но работает довольно шустро

Для интереса - дамп tracy. Его можно открыть клиентом tracy с версии 0.1.0
Немного о дальнейших планах. Сейчас моей целью будет по максимуму стабилизировать движок и платформы. Для этого хочу поднять поддержку ios, android, linux. А так же добавить CI на гитхаб. Причесать кодстайл, поправить ошибки и просто старые места. По ходу дела поправить биндинг в скрипты, сделать его более оптимизированным. По возможности прикрутить spine анимации.

В итоге хочется получить рабочую бета-версию, чтобы уже на ней попробовать делать игры. Так же пробовать внедрить это в какие-нибудь студии. Опыт показал что решение может быть интересным и полезным для других, но нужно быстро и резко внедрять движок, чтобы сразу получать профит. С багами и нестабильным кодом это не получается
На выходных прикрутил элементарный CI, который хочется развить до полноценной проверки всех интересующих платформ. Пока что завел MSVS билд под windows, тк на текущий момент только он живой.

Попробовал пару сервисов appveyor и github actions. Оба весьма похожи, но гитхаб нативно интегрирован в гитхаб, поэтому в итоге выбрал его. Теперь в репозитории o2 красуется бейджик о статусе сборки. Теперь хочу восстановить linux платформу и так же загнать ее в CI.
Немного об удобстве CI через github actions/appveyor. У них похожие концепции - сборка настраивается через конфигурационный файл.

Я всегда был сторонником графического интерфейса, а не текстового. Поэтому не люблю всякие терминалы, CMAKE и подобные вещи. В github action то же самое, там все настройки в текстовом файле с определенным синтаксисом. Сходу навязывается некий синтаксис, то есть правила, которые необходимо соблюдать. Нарушишь эти правила - беда. Это плохо признак UX, ведь это прямое нарушение принципа защиты от ошибки. Все мы люди, можем опечататься, что-то забыть, что поломает систему. Гораздо лучше интерфейсы, где возможность ошибки прост исключена.

Так же текстовый интерфейсы по умолчанию не интуитивны. В GUI можно увидеть тултипы, выпадающие списки, имена полей, то есть можно методом тыка разобраться в функциональности. В текстовом варианте у тебя максимум есть команда —help, которая вываливает тебе тонну текста практически без форматирования.

С github actions у меня сразу такая проблема и возникла, при выборе типа агента на котором запускать сборку. По умолчанию там Linux, мне нужно было выбрать windows.... и я просто не знал что там написать. Пришлось лезть в документацию, специально искать нужную информацию, то есть делать лишние действия. Если бы это был GUI, то у меня был бы выпадающий список с возможными вариантами агентов, и я бы просто выбрал за 5 секунд вместо гугления 5 минут. Но в итоге скрипт мне сгенерировал Chat GPT :)

Мне кажется так происходит из-за проф деформации программистов. Так уж вышло что мы 95% времени работаем с текстом. А так же со сложными вещами. Поэтому идем по накатанной: запихиваем все в текст и не стараемся упростить. Делать GUI как правило лениво, это гораздо дольше чем распарсить аргументы командной строки или текстовый файл.

Но на мой взгляд сейчас со всякими GPT это уже не проблема, достаточно грубо описать функциональность и получить почти работающий GUI код. Даже с генерацией python-gui хорошо справляется ChatGPT, если стравить ему текст из команды —help
Апдейт! Завел linux, теперь можно и там запускать игру или редактор. Особо ничего нового со стороны кода не было сделано, т.к. linux был поддержан еще прошлым летом для ребят из гемблинга.

Но пришлось поправить некоторые ошибки компиляции после перехода на умные указатели. Одна из них потребовалась из-за разницы работы компиляторов. При создании ссылки, у типа проверяется наличие метода PostRefConstruct. Если такой есть, он вызывается. Проблема была в том, что этот метода приватный, а type trait, Определяющий наличие типа - в глобальном неймспейсе. Компилятору MSVS все равно, и он успешно компилировал код, говоря что метод есть. На gcc все строже, и т.к. метод приватный, то взять указатель на функцию невозможно, а значит type trait возвращал false при проверке наличия метода. Из-за этого он не вызывался и где-то там далее поведение программы портилось (а точнее в системе ассетов).

Решилось все небольшим рефакторингом. Метод конструирования ссылки был friend'ом классов, которые имели приватный PostRefConstruct. По этому же принципу вся инфраструктура по созданию ссылки, в т.ч. type trait, были вынесены в отдельный класс, который уже и дружился с типами с PostRefConstruct.

Сейчас еще остаются проблемы на linux платформе: есть креш на закрытии редактора, не поддерживается сохранение размеров окна между запусками, не меняется тип курсора и другие небольшие недоработки. Все это буду делать планомерно, цель завести Linux полноценной платформой, где запускается редактор со всем нужным функционалом.
О, и конечно же, завел CI на github чтобы проверять собираемость linux. Есть что-то в этих бейджиках на странице проекта 😃
image_2024-08-18_12-17-55.png
14.9 KB
когда перемудрил с кодировкой...
интересный код обнаружил у себя в проекте... как говорится, когда ищешь виноватого, главное не выйти на себя. Но тут, к сожалению, вариантов нет
2024-10-10 11-15-43.gif
16.7 MB
Небольшой апдейт! Сейчас занимаюсь системой частиц, о ней будет попозже. А пока - color picker!

Казалось бы, простая штука - сделать поля ввода RGBA, вывести цвет... Но это уже прошлый век какой-то. Хочется ползунков и интерактивности.

Сделал у себя довольно классический вид, совместив все нужное:
есть классическая зона выбора цвета на пространстве от темного к светлому
есть классические RGBA
есть HSL (hue, saturation, lightness) - оч удобный способ выбирать цвет, порой гораздо понятнее обычных RGB
есть текстовые поля ввода RGBA и HEX представления цвета

Чего еще хотелось бы:
- цветовые пресеты
- выбор цвета пипеткой с экрана

Немного подробностей, где получились интересные штуки.

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

Работа с цветом в формате HSL усложнена тем, что эти параметры высчитываются из RGB, а значит они ведут себя нестабильно. Например, если увести цвет в черный, то уже фиг пойми какие там HSL, и формулы начинают выдавать разные результаты. Но ведь юзер, двигая один из ползунков HSL не хочет видеть как все остальные ползунки сбиваются. Поэтому при изменении одного из параметров HSL, остальные фиксируются, чтобы их не "увело"

Ну а в остальном обычный GUI, который на o2 делать уже довольно легко. Весь color picker занимает около 450 строк кода
О, курва!

Добавляя новые эффекты для системы частиц, обнаружил что мои курвы, то есть кривые, не совсем подходят. Дело в том, что кривая у меня - это график безье, где значению по X соответствует только одно значение по Y. То есть можно прямо в редакторе задать какую-то функцию от руки. Под это уже давно есть простенький редактор

Но для системы частиц зачастую нужна некая вариативность, "рандом" при обработке частицы. Чтобы каждая частица летела немного отлично от других, за счет чего достигается эффект. Одно значение по Y для такого не подходит, и первая мысль была - сделать две курвы! А значения между ними рандомно интерполировать. Но это не очень хорошо выглядело в редакторе. Отдельно редактировать две кривые совсем не удобно, а хочется чтобы они были вместе, да еще и промежуток между ними подкрашивался для визуализации, какое значение рандома может выпасть

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

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

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

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