Telegram Web
Kickstarter стартовал!
#лайт
Между тем мы позавчера вышли на Kickstarter!
Это и есть главаная причина, почему мы кранчили так сильно последнее время. Особо ничего по этому поводу добавить нечего, все подробности по проекту на странице кампании. Буду рад, если поможете собрать сумму или зашарите куда-нибудь. А мы теперь работаем над апдейтами и первой публичной демкой (а там прилично мне работы по оптимизации, сборке билдов и локализации). Stay tuned :)
Поздравляшки, чатик, обнимашки
#лайт
Выше на канале я грозился, что худею, но сейчас мне можно тортик: я праздную тысячу подписчиков. Во-первых, это психологически важная отметка, после которой у меня отпали вопросы о том, стоит ли продолжать вести блог дальше или это никому неинтересно. Во-вторых, я решил в честь этого завести чатик. Я и сам подумывал так сделать, но сегодня меня напрямую попросили об этом в сообществе игры Encased. Так что я сходил в магазин, купил тортик, и вуаля: группа Alprog I/O. Теперь те, кто писал свои вопросы и комментарии мне в личку (за что им, кстати, большое спасибо), смогут это делать в специально отведённой группе.

А ещё сегодня мы набрали 50% от основной цели на Kickstarter, так что тем более незазорно отпраздновать. Вы добавляйтесь там в чатик, а я пока тортик съем.
Что-то вроде ECS, но вообще не оно
#код
Этот пост я пишу из Исландии. Коротко сформулировать впечатления от этой страны можно так: красиво, но бессмысленно. Это не в обиду исландцам сказано, просто здесь меня постоянно преследует немой риторический вопрос «зачем вообще селиться на этом суровом куске земли?» Местность вокруг часто больше походит на терраформированный Марс, чем, собственно, на Землю. Но сейчас я «под куполом», то есть в тёплой гостинице, а потому хочется поговорить о чём-то противоположном: пусть не очень красивом, но зато функциональном.

Таковой является, например, наша компонентная система. Последние полгода стоит кому-нибудь заговорить о компонентах, как все сразу вспоминают ECS (Entity Component System), и на первый взгляд то, что написал я, довольно похоже; но на самом деле не имеет с ним ничего общего.

Но давайте по порядку. В Unity много лет были GameObject’ы, которые содержали Component’ы. Это привычная всем, но неэффективная модель. Некоторые даже ошибочно называли её ECS, хотя ей там и не пахло. Настоящая же ECS предполагает 3 вещи: это, собственно, Entity (сущность), Component и System. Причём Entity, в отличие от GameObject’ов — это не объекты. Это просто целочисленные id. То есть просто номера сущностей, которые сами по себе никаких компонентов не хранят. Напротив, каждый компонент «знает», к какой сущности он закреплён. И что немаловажно, компоненты одного типа лежат в одном месте (в одном массиве) и представляют собой только данные. А вся логика находится в системах, которые обрабатывают компоненты только определённого типа. Таким образом логика всегда сводится к тому, чтобы пробежаться по одному или нескольким массивам компонент и выполнить над каждым элементом какие-то однообразные задачи. Благодаря тому, что однотипные данные лежат в памяти друг за другом последовательно, эта задача прекрасно параллелится за счёт векторизации и крайне cache-friendly, что очень и очень хорошо для производительности. В этом и есть основная суть ECS.

В Unity для её внедрения привлекли самого Майка Актона (одного из самых главных идеологов Data oriented design и ECS в частности). Я не смотрел, что у них в итоге получилось, но судя по имени, там теперь действительно всё грамотно в кои-то веки. Но мы стартовали проект, когда этого ещё не было в стабильной версии Unity, поэтому начали разработку на своих компонентах.

У нас тоже есть Entity, но это не номера, а полноценные объекты, которые содержат наши компоненты (мы их зовём модули), а систем нет вообще. По сути это больше даже похоже на старую модель компонентов Unity и тоже совершенно не про производительность. Зачем же тогда надо было это переизобретать? Дело в том, что одним из самых первых и главных стратегических решений в Encased было как можно сильнее отвязаться от сцен и gameobject’ов. Наши Entity и модули — это данные в чистом виде. Игра может играть сама в себя безо всякой визуализации (с небольшими оговорками), а также легко и безболезненно сохранять или загружать своё состояние. С gameobject’ами, как вы понимаете, это сделать было бы в разы сложнее, так как сериализация в Unity это та ещё Песнь пламени и льда (про наше решение как-нибудь напишу отдельный пост). А вся визуализация у нас происходит за счёт размещения на сцене Actor’ов, которые «играют» роль сущностей. У актора на каждый тип модуля сущности есть ModuleSync (если это необходимо), который синхронизирует визуальное представление. Причём могут быть различные Sync для Play- и EditMode.

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

Обсудить
Сериализация. Часть 1: Предисловие
#кодище
На прошлый мой пост были комментарии в чатике о том, что как-то слишком заморочено всё у меня. Поэтому я решил вас не томить, и начать уже рассказывать о действительно мудрёной системе — о нашей сериализации. Здесь уж ни в какие лимиты 4000 знаков я точно не влезу, поэтому рассказ будет разбит на несколько частей.

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

Первым делом, конечно, проверил, не поменялось ли чего в стандартной сериализации Unity. А там без изменений: мрак и тихий ужас. Плохо всё: жёсткая привязка к UnityScriptableObject, нет поддержки абстрактных или базовых классов (то есть никакого наследования, только конкретные типы), нет поддержки свойств, отвратительный резолвер ссылок, который всегда резервирует место под целый объект. А если объект содержит ссылки на объект того же типа, то резервирует место до 7 уровня вложенности. А это выделение места под 2187 объектов только для трёх ссылок (3⁷)! Я правда не понимаю, как на этом можно делать что-то сколько бы то ни было серьёзное.

Расширять это безумие можно только жутчайшими костылями в виде скрытого string поля, которое заполняется байтами и парсится самостоятельно на ISerializationCallback’и. Именно так поступает популярный плагин Odin, на которого я поначалу возлагал большие надежды. Да, приходится наследоваться от его классов; да, твои классы от этого пухнут байт на 20 каждый (даже мелкие), но я готов был мириться даже с этим, лишь бы это сэкономило нам время, если бы не одно «но». Как выяснилось, поддержка сериализации свойств в Odin’е распространяется только на auto-properties, то есть на те свойства, что имеют дефолтный геттер и сеттер; а никаких кастомных действий в сериализацию пропихнуть не удастся. Без чего вся затея теряет всякий смысл. И это, заметьте, самое лучшее решение из Asset Store, которое сами Unity Technologies отмечали. Так что от него и ему подобных пришлось отказаться.

В этом месте средний программист прикрутил бы первую попавшуюся json-библиотечку (и потом бы мучался с ней в той или иной степени), но большинство из них меня по разным причинам не устраивали: где-то нет исходников, где-то уродская атрибуция, где-то нет бинарного формата. А если и есть, то имена полей всё равно пишутся много раз текстом в файл.

То ли дело Protobuf (протокол сериализации от google), который решает все эти проблемы и даже больше. Для тех, кто не знает, его фишка в том, что все сериализуемые поля класса помечаются атрибутом с уникальным номером. Привязка полей идёт именно по этому номеру, поэтому можно спокойно переименовывать, добавлять или удалять любые поля, не беспокоясь при этом об обратной совместимости сейвов. К тому же я уже работал с ним в .Net и горя не знал (кроме пары мелочей). Но беда пришла откуда не ждали. Мне потребовалось сделать условную сериализацию: ноды верхнего уровня сериализуются полностью, а вложенные объекты сохраняются как ссылки (об этом в следующих частях). И в protobuf просто не оказалось способа задать такое поведение без копипаста или появления лишнего редиректа. Казалось бы, не бывает слова «нельзя» — возьми исходники да поправь — но выяснилось, что dll написана на C# 7, а в Unity даже 6 был тогда только в бете. Пересобирать её каждый раз на стороне жуть как не хотелось, поэтому я продолжил поиски.

To be continued… Обсудить
Благотворительный пиарчик
#лайт
Я не очень много читаю каналов в телеграмме, но сегодня я хотел бы немножко попиарить два канала-малыша, на которых меньше 30 подписчиков, просто потому, что они мне нравятся. А ещё потому, что такие записи это лёгкий способ создания контента на своём канале :)

Во-первых, канал дружественных мне ребят WS Games. Это двое разработчиков, которые делают в свободное время игру в стиле gameboy на MonoGame. Там нет портянок про архитектуру и программирование, как у меня; там лишь прогресс создания, скриншоты и фотографии с различных выставок. Но если вы тоже инди-разработчик или хотите им стать, то вам наверняка будет интересно следить за их прогрессом. Подписывайтесь, они клёвые.

Во-вторых, канал Землякова. Это канал-спутник для одноимённого youtube-канала, и он вообще не про геймдев, а про астрономию. Признаться, я редко смотрю там ролики, так как они не всегда попадают в сферу моих интересов (мне, например, больше интересна космология, чем, скажем, космонавтика). Но мне очень нравится то, что автор делает в плане научпопа; и было бы преступлением не воспользоваться случаем, чтобы помочь ей с подписчиками. Уверен, среди моей аудитории найдётся много любителей науки о Вселенной.

Обсудить
Сериализация. Часть 2: фатальный недостаток MessagePack
#кодище
Продолжим рассказ про сериализацию. Перебрав кучу вариантов, я наконец обратил внимание на проекты чувака по имени Yoshifumi Kawai, более известного как neuecc. Этот японец написал целый выводок сериализаторов под разные задачи для .Net: UTF8Json, ZeroFormatter, MessagePack.

UTF8Json предоставляет человекочитаемый, но медленный формат; ZeroFormatter заточен на скорость, но создаёт большие файлы; а MessagePack являет собой эдакий Protobuf на максималках. У него достаточно высокая скорость, но при этом крошечный бинарный формат и поддерживается версионность. Более того, поскольку он сразу писался именно для .Net, а не универсально, он лишён недостатка Protobuf, чья система типов не может отличить null от пустой коллекции.

Внутренности MessagePack тоже выше всяких похвал и оптимизированы просто донельзя. Функции сериализации классов там генерируются на старте прямо из op-кодов IL, а API использования не вызывает никаких аллокаций. Более того, инты с мелкими значениями пишут в файл меньше 4 байт (7 бит каждого байта отводится на число, а последний бит указывает, влезло ли число или нужно прочитать следующий байт). В общем, мечта.

Но если с низким уровнем всё хорошо, то наверху обнаружились фатальные недостатки. Чтобы понять, в чём проблема, нужно объяснить тамошний формат. MessagePack сериализует объекты в некоторое подобие BSON (Binary JSON). То есть он записывает последовательность бинарных токенов, таких как «начало массива из N элементов», «начало ассоциативного массива», «число», «строка» и т.д. Если прочитать это, то можно получить строку в формате очень похожем на JSON (с небольшими отличиями).

MessagePack также имеет два варианта атрибуции. Вариант с текстовыми ключами:

class A
{
[Key("foo")] string Foo = "Text";
[Key("bar")] int Bar = 777;
}


создаст следующую JSON-строку {"foo":"Text","bar":777}, что, как вы понимаете, не очень хорошо, так как названия ключей будут писаться в файл сотни раз. Поэтому есть аттрибуция интами:

class A
{
[Key(0)] string Foo = "Text";
[Key(3)] int Bar = 777;
}


что породит простой массив [“Text”, null, null, 777], где номер поля обозначает его положение внутри массива (что может приводить к дырам).

Также есть возможность атрибутами перечислить у абстрактного класса все виды его конкретных типов. И тогда при записи сначала будет номер конкретного типа, а уже потом во вложенном массиве содержимое его полей:

[5,[“Text”, null, null, 777]]


Так что же не так с форматом? Во-первых, в такой схеме полиморфизм наследования работает только для абстрактных классов. Если же пытаться сохранить массив конкретных типов (но от которых можно наследоваться), то информация о реальных типах потеряется. Во-вторых, MessagePack предполагает, что ключи атрибуции полей уникальны не только для самого класса, но и для всех его базовых классов. А это крайне тяжело гарантировать, если дерево наследования будет разрастаться, либо же придётся делать большие зазоры между номерами, что приведёт к сильному увеличению бинаря.

Благо, библиотека имеет MIT-лицензию и отлично расширяется. Поэтому я написал полностью свою атрибуцию, разбор её рефлекшеном и кастомные резолверы и форматтеры для записи в файл. Каждый конкретный класс у меня получает через атрибут свой глобальный номер (новое значение в большом enum’e), а атрибуция полей уникальна только в рамках данного подкласса. Таким образом, в файл пишется примерно следующее:

[5,[null, 17, 43],[88, false],[“Text”, null, null, 777]]


Где, 5 это номер класса, а каждый «вагончик» — это поля каждого класса от базового до конкретного. Причём номер пишется только в тех ситуациях, когда действительно необходим, а вагончики записываются только для классов, которые реально имеют данные.

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

To be continued… Обсудить
Unigine 2 в массы!
#лайт
На днях движок Unigine 2 наконец-то развернулся лицом к мелким разработчикам игр и ввёл новую модель распространения. Unigine — это особенный игровой движок. Он сделан разработчиками из Томска и я помню его, можно сказать, с пелёнок. Я помню ту тему на gamedev.ru в 2005 (а это, между прочим, времена Half-Life 2 и Doom 3), в которой человек под ником frustum показывал свои первые наработки движка. Помню ту демку с костром возле какой-то кирпичной стены; и помню тот скепсис, что вызывали подобные начинания.

Но теперь это здоровенная Unigine Corp. с официальным представительством в Шанхае, кучей проектов и картинкой, способной потягаться с Unreal и CryEngine. Но вы, возможно, ничего про него не слышали и сейчас недоумеваете, как же так вышло. А дело в том, что примерно в то время, когда игровые движки большой тройки ринулись на инди-рынок (2013-2014), Юниджайн обратили внимание на кардинально иной сектор — на всякого рода профессиональные симуляторы: авиа и космотренажёры, различного рода визуализации городского траффика и водного транспорта, приложения САПР и архитектуры, и прочая, прочая. Обратили внимание и очень плотно обосновались в этой нише.

Но хотя неигровые проекты приносят компании более 90% дохода, игры на этом движке также выходили. Вы могли слышать о таких проектах, как OilRush, Cradle, Sumoman. Но это были либо внутренние разработки студии (OilRush), либо проекты, созданные выходцами из Unigine или людьми, имеющими тесный контакт с компанией. А для широкой же общественности движок был практически недоступен, так как лицензирование каждой конкретной игры рассматривалось отдельно через запрос. Такие кейсы тоже были, но их немного и это ещё менее известные проекты.

Работа с каждым клиентом индивидуально и раздача своего движка практически всем желающим — это две принципиально разные модели, требующие разных подходов к документации, тех.поддержке и инструментарию. Но вот наконец-то в Unigine нашли время, силы и политическую волю сделать дешёвую подписку. 99$ за один месяц или 83$ при подписке сразу на год. Это может показаться дорого, особенно, в сравнении с Unity, но если сравнивать с Unreal или Cry с их 5% с продаж, то это сильно дешевле. И это вполне по бюджету любому проекту, в котором на зарплате есть хотя бы 2-3 человека.

Совсем голодное инди, конечно, позволить себе такой движок вряд ли сможет; хотя есть возможность запросить пробную версию. Я себе запросил. Если дадут пощупать, потом обязательно отпишу о впечатлениях.

Обсудить
Курсы по геймдизайну
#реклама
Один из самых часто задаваемых вопросов ко мне — «с чего начинать?» Для программистов я собираюсь написать когда-нибудь подробный пост с оптимальным на мой взгляд планом того, что учить и в какой последовательности. А вот с теми, кто хочет стать геймдизайнером, немного сложнее. Обычно я советую попробовать делать моды, устроиться в существующую компанию сперва левелдизайнером, либо же пройти какие-нибудь курсы по геймдизайну.

Один из таких курсов как раз стартует в следующем месяце на SkillFactory. В отличие от многих других, он сосредоточен именно на практике. Курс рассчитан на четыре месяца, в течение которых ученики займутся реальной разработкой дизайн-документа под руководством Руслана Казанцева, лида финской студии BON Games (ранее — сеньора в GameInsight). Программа обучения из 16 занятий затрагивает широкий спектр тем: от разработки игровой механики до грамотного планирования монетизации и продвижения проекта.

Записаться или прочитать подробнее можно здесь: https://vk.cc/8CnWpe
Сериализация. Часть 3: фичи
#кодище
В предыдущих постах я писал, почему мы выбрали MessagePack, и как переписали атрибуцию к нему. Пришло время закончить тему разговором о продвинутых фичах нашей сериализации.

1. Ссылки внутри файла. Не секрет, что при сериализации сложных данных легко нарваться на рекурсивные ссылки. Для ссылок внутри файла наше решение просто и элегантно: когда сериализатор начинает писать в файл новый объект, он присваивает ему порядковый номер и записывает в файл целиком; если же сериализатор повторно натыкается на этот же объект, он записывает только его порядковый номер. При чтении файла объекты попадаются в том же порядке, что позволяет легко восстановить связи. К моменту, когда десериализатор наткнётся на номер-ссылку, объект уже гарантированно будет создан.

2. Ссылки в другой файл. Иногда в сериализации требуется сослаться на объект, который хранится в другом месте. Для этого существуют GuidObject’ы, которые помимо всего прочего имеют уникальные Guid’ы. Когда GuidObject сериализуется в файл первый раз, у него есть выбор в зависимости от настроек: сериализовать объект полностью или записать только Guid, полагая, что тело будет записано в какой-то другой файл. Обратите внимание, что в обоих случаях, когда объект встретится повторно в этом же файле, Guid повторно записан не будет. Вместо него как обычно будет записан порядковый номер внутри файла.

Таким образом, один и тот же Guid никогда не пишется дважды в один файл, поэтому можно использовать довольно длинные Guid’ы и не бояться, что файлы будут пухнуть. Мы генерируем стандартные 128-битные Guid’ы, что позволяет создавать новые GuidObject’ы на разных машинах и не бояться конфликтов.

3. Склеивание. Фишка, когда при десериализации GuidObject создаётся пустым, а его поля заполняются уже позже из другого файла, делает возможным пойти дальше и разнести содержимое одного GuidObject по разным файлам.

Мы пользуемся этим, чтобы отделить статичные данные от сейва игрока. Например, у класса Location поля названия и координат на карте помечены атрибутом [StaticKey], а статус исследованности помечен атрибутом [RuntimeKey]. Таким образом состояние игрового мира хранится в двух больших бинарных файлах, а при загрузке информация из них «склеивается».

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

4. Распределённая сериализация. В релизе, как уже было сказано выше, мир загружается из двух больших бинарных файлов, но это неудобно во время разработки (как минимум с точки зрения разрешения конфликтов). Поэтому у нас есть специальный режим сериализации, когда каждый GuidObject кладётся как отдельный текстовый файл в специальную папочку. Это позволяет резолвить конфликты на уровне конкретного объекта, а также добавлять/удалять объекты в этой папке с разных машин.

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

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

На этом цикл постов по сериализации завершён. Ух, долго же я не мог собраться с силами закончить его. Надо постараться впредь не делать таких больших пауз. Кстати, на следующей неделе буду в Питере, собираюсь забежать на митап по С++ (можно словиться, если хотите).

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

Лично меня накрыло чуть больше года назад. На функциональщину я так и не подсел, а в императиве всё уже казалось скучным и привычным. Конечно, кругом были десятки ещё неизученных штук, но ничего не интриговало, нигде не было чего-то принципиально нового. И тут я узнал про проект метаклассов в С++. Вау. Это как проект открытия червоточины в параллельную вселенную. Вау. Амбициозная задумка, которая невозможна на текущем уровне развития технологий, но, чёрт возьми, я хочу дожить до момента, когда это станет реальностью, и попробовать самому.

Чтобы реализовать метаклассы, нужно сперва сделать рефлекшн, расширить выполнение компайл-тайм кода и code-injection, на что уйдёт уйма лет и митингов, но результат стоит того, чтобы к нему стремиться. Метаклассы расширяют представление о возможном также, как первое глубокое погружение в шаблоны (не путать с генериками .Net; они — детский лепет). А, может быть, даже ещё больше.

Кто-то может сказать, дескать, тоже мне, удивил — всё это уже было в Python, Ruby и другой хипстоте. Честно признаюсь, не смотрел, как это выглядит в вышеупомянутых языках, но крышесносящей смесь по моему мнению становится лишь в связке с С++. Высокопроизводительный консервативный и, если хотите, задротский язык, который терпеть не может платить за то, что не использует, казалось бы, обречён быть неповоротливым и жестоким. Но он полностью преображается с метаклассами и при этом не изменяет своим фундаментальным принципам. Это полностью меняет правила игры и должно стать для многих вторым открытием языка (именно поэтому я верю, что с++ не исчезнет из топов даже в долгосрочной перспективе).

Итак, что же это за хрень, которую я так долго нахваливаю? Суть в общем-то сводится к тому, чтобы позволить, не выходя за пределы языка, обрабатывать классы во время компиляции. Например, пробежаться по всем членам класса и изменить их модификаторы доступа, добавить или удалить конструкторы, автоматически сгенерировать перегрузки функций сравнения или бросить ворнинги, если чего-то не хватает. Если поразмышлять немного над тем, что это даёт, то можно понять, что это открывает поистине потрясающие возможности: мы сможем сами ввести в язык понятие, скажем, valuetype (со всеми вытекающими требованиями) или interface. При этом не будет бесконечных споров в комитете, что именно правильно подразумевать под этими словами и достаточно ли это универсально. Комьюнити само со временем наработает всевозможные паттерны, лучшие из которых впоследствии войдут в различные библиотеки, и никто не уйдёт обиженным. То есть мало того, что возможность завести valuetype и иже с ними сама по себе привлекательна, так ещё и сам язык начнёт эволюционировать динамичнее.

Впрочем, мне через 5 минут уже пора на посадку в самолёт, а вам более подробно (и чертовски наглядно) всё объяснит сам Херб Саттер в этом видео. Ну а самые любознательные могут также заглянуть в соответствующий пропозал.

Обсудить
2025/07/10 08:14:21
Back to Top
HTML Embed Code: