GameDev: разработка на Unity3D
443 subscribers
6 photos
2 videos
7 files
40 links
Все для успешного развития разработчика в геймдев: от junior до CTO. SOLID на практике, IOC, DI. Новые паттерны для gamedev. Личный опыт.

Заявка на разбор тестовых https://forms.gle/kqVPv1jWT97Bkps9A
Download Telegram
#опрос

Приветствую, друзья 👋


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

Вопрос звучал:
Есть 2 класса: логика и представление. Один класс использует методы другого. Чтобы ослабить зацепление, в каждом классе выделяем интерфейс и реализуем использование классов через вызовы методов интерфейсной ссылки. Оптимален ли подход? Совпадает с вашим?

Результаты 👇
30% применяют описанный подход
47% используют альтернативные способы ослабления зацепления в коде
23% затруднились с ответом

Итак, приступим 🚀

Что касается приведенного в опросе подхода: на мой взгляд, он оптимален там, где редко меняются бизнес процессы.
Поэтому, как правильно отметил @GlassOfDream 👌в игровой индустрии - такой подход - скорее утопичен и зависит от многих факторов: сложность и стадия проекта, количество человек в команде и т.д.

Также вы перечислили другие варианты, отражающие разнообразие подходов и методов:
- Прямая связь, если в этом месте не нужен полиморфизм. С защитой от дурака в виде автотестов с рефлексией.
- Через публичный api-интерфейс в классах представления и логики и создание третьего класса ~адаптер/контроллер, который знает о логике и представлении и связывает их.
- Через ивенты. Логика отправляет ивент, подписчики слушают и обрабатывают.
- Перенос зависимостей в аргументы метода.
- Использовать ECS.

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


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


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

Спасибо за участие в опросе
Хорошего вечера 👍
#контент #слои_абстракции
Приветствую, друзья
 👋

Сегодня говорим про контент.

Контент является неотъемлемой частью игровых проектов на Unity C#.
Представляет собой информацию, например: настройки уровней, данные о персонажах, объекты окружения и многое другое.

Контент - это отдельный слой, который представляет из себя иерархию неизменяемых классов (то есть ни одно свойство класса не меняется в runtime), базовый предок - класс Content (см. иллюстрацию Иерархия контентных классов).

В данном контексте контент относится к описательной части, которая не является непосредственно ресурсами игры, но может содержать ссылки на ресурсы.

Контент может храниться в разных местах в зависимости от его характера и требований проекта.
Как всегда при выборе места хранения опираемся на тип контента и требования проекта.

Рассмотрим возможные варианты хранения контента:
- встроенный в билд.
Обновление контента будет требовать пересборку проекта
  - на сервере в виде статики.
Позволяет обновлять контент независимо от приложения, но требует сетевого доступа и управления скачиванием. Отдельное внимание следует уделить версионированию, так как с течением времени контент может эволюционировать и его новые версии могут не поддерживаться старыми версиями клиента
  - внешние источники: сервисы или хранилища, как пример Google Таблицы.
Подход позволяет обновлять контент в реальном времени, не обновляя само приложение. Мы получаем динамический контент в режиме реального времени. Подход может потребовать дополнительной настройки и интеграции.
Также необходимо помнить про версионирование
  - смешанный. Контент зашивается в проект с возможностью докачки/обновления контента.
Основное преимущество - для старта приложения не нужна загрузка по сети, т.е. приложение будет работать при отсутствии интернета.

Все поля контентных классов, как правило, публичные (речь о полях, используемых в игровой логике).

Если говорить про иерархию, контент - отдельный слой. Представляет иерархию неизменяемых классов: ни одно свойство класса не меняется в runtime. Базовый предок - класс Content (см. иллюстрацию “Иерархия контентных классов”).
Если представить иерархию в виде дерева, то для каждого из узлов дерева будут выделены типизированные коллекции с возможностью доступа по ключу. Ключ - id инстанса контента.

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

Рассмотрим на примере иллюстрации:
- Content - базовый класс контента
- Enemy - наследник Content
- GroudEnemy - наследник Enemy
- WaterEnemy - наследник Enemy.
На каждом уровне дерева типы могут содержать свой набор полей.
#контент #слои_абстракции
Контент продолжение 👇

Подобная структура позволяет разработчикам определить общие черты и функциональность для коллекций контента.

Для каждого уровня иерархии классов контента (Content, Enemy, GroundEnemy, WaterEnemy) выделяем типизированные коллекции, позволяющие обращаться к объектам по ключу.
Отдельно выделяется класс ContentCollection, который содержит все эти коллекции.

В данном случае, коллекции "contents", "enemies", "groundEnemies" и "waterEnemies" содержат соответствующие типы контента:
- contents - коллекция (тип элементов - Content), которая содержит все типы контента (классы, которые могут быть приведены к классу Content: Content, Enemy, GroundEnemy, WaterEnemy)
- enemies - коллекция (тип элементов - Enemy), которая содержит все типы контента (классы, которые могут быть приведены к классу Enemy, в данном случае - это классы Enemy, GroudEnemy, WaterEnemy)
- groudEnemies - коллекция (тип элементов - GroundEnemy), которая содержит все типы контента (классы, которые могут быть приведены к классу GroundEnemy, в данном случае - это только класс GroundEnemy)
- waterEnemies - коллекция (тип элементов - WaterEnemy), которая содержит все типы контента (классы, которые могут быть приведены к классу WaterEnemy, в данном случае - это только класс WaterEnemy).

Так, например, если нам нужен конкретный враг из groundEnemies (например с id - 17), мы можем получить его так:
contentCollection.groundEnemies[17]

Или враг, без уточнения типа, то можем получить его так:
contentCollection.enemies[17]


Или если нам просто нужен контент, то так:
contentCollection.contents[17]


Такой подход обеспечивает строгую типизацию и предоставляет доступ к конкретным типам объектов без необходимости использования операторов "is" и "as", что повышает эффективность и удобство разработки.

Преимущества использования типизированных коллекций в проекте:
- Улучшенная читаемость кода: использование типизированных коллекций упрощает понимание кода и его намерений.
- Разработчикам не нужно использовать операторы "is" и "as" для проверки типов объектов или дополнительно приводить типы.
- Код становится более ясным и легко читаемым.
- Увеличение производительности: избегая динамического приведения типов, разработчики могут сократить накладные расходы на выполнение кода и повысить производительность приложения.
- Упрощение сопровождения: Типизированные коллекции делают код более поддерживаемым.
- При добавлении новых типов контента или изменении структуры иерархии классов, разработчики могут сосредоточиться на обновлении соответствующих типизированных коллекций, а не искать и изменять динамический код в разных частях проекта.

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

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

Благодарю за внимание!
Вопросы в комментариях приветствуются!
#архитектура #состояния #контент #чистый_код

Приветствую, друзья
👋

Сегодня говорим про состояния игровых объектов в C# Unity и о ключевой роли управления состояниями.

Состояния представляют собой различные конфигурации и параметры, которые определяют поведение и визуальное представление объектов в игре.
Эффективное управление состояниями позволяет упростить разработку и обеспечивает легкость восстановления сохраненных данных в игровом процессе.

Зачастую разработчики не отделяют слой представления и слой данных (контента) от слоя состояний: и данные, и состояния помещаются в MonoBehaviour, что на практике не является лучшим/оптимальным решением.

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

Есть простой тест, который покажет насколько эффективно реализовано управление состояниями объектов. Для этого необходимо проверить насколько легко выбросить оружие со всеми его текущими характеристиками и следом подобрать его же либо выпавшее у противника.

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

Каждый инстанс контента описывается уникальным набором данных и может иметь своё состояние, которое определяет текущие параметры и свойства этого контента в игровом мире.
Контентные классы могут быть представлены как:
- объекты сцены,
- персонажи, оружие,
- предметы и т.д.

Состояния позволяют отделить поведение объектов от самих объектов. Это упрощает логику и облегчает поддержку кода.

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

Остановлюсь подробнее на рассмотрении организации контентных классов с поддерживаемыми состояниями.

В контентных классах, для которых поддерживаются состояния, должен быть выделен метод CreateState.
Задача метода - создать необходимый класс с состоянием.
Данный метод потребуется игровой логике, когда будем создавать состояние первый раз.

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

Состояние игровых объектов - это отдельный слой. Представляет из себя иерархию изменяемых классов, базовый предок - класс ContentState (см. иллюстрацию Иерархия классов состояний).

ContentState, как базовый предок, должен содержать поля:
- id - значение может быть создано как на клиенте, так и на сервере
- contentId - id контента, к которому относится состояние
Для производных от базового ContentState класса классов должен содержаться метод - GetНазваниеКонтентногоКласса (см. иллюстрацию).

Разберем на примере иерархию классов состояний:
Класс EnemyState - состояние врага
У данного класса должен присутствовать метод GetEnemy - получение объекта контента из контентной коллекции.
Предположим, что враги бывают разных типов с разным состоянием свойств для каждого из типов.
Класс GroundEnemyState - состояние наземных врагов.
У данного класса должен быть метод GetGroundEnemy - получение объекта контента из контентной коллекции.

Класс WaterEnemyState - состояние водных врагов. Должен содержать метод GetWaterEnemy, и т.д.

Продолжение 👇
#состояния_продолжение #архитектура #состояния #контент #чистый_код

Состояние игровых объектов продолжение
👇

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

Что касается состояние самого игрока, информация о нем должна находиться в профиле игрока, здесь речь о: имени, здоровье, энергии, инвентаре, купленных предметах, и т.д.

Если говорить про реализацию классов состояний, то как правило все поля состояний - реактивные объекты (ReactiveProperty, ReactiveCollection, и т.д.).
Методы с игровой логикой в классах состояний игровых объектов присутствовать не должны.

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

При запуске игры, после загрузки профиля игрока и профиля игрового мира (или его части) соответствующие объекты состояний передаются в необходимые узлы composition tree (Entity) вспомнить структуру поможет иллюстрация.
За дальнейшую обработку состояний игровых объектов будут отвечать соответствующие Pm.

Приведем упрощенный пример реализации иерархии классов состояний для контента, о котором мы писали в предыдущей статье про контент.
Итак 👇:
Пусть у каждого врага (где враг - контентный класс Enemy, а класс состояния - EnemyState) в состоянии есть:
- health - здоровье (значение от 0 до 100)
для GroundEnemy наземных врагов:
- не будет собственных параметров.
для WaterEnemy водных врагов:
- isStormResistant - устойчивость к шторму.

Тогда реализация классов состояний для контента будет иметь вид 👇

public class ContentState
{
public UInt32 contentId;
public readonly ReactiveProperty<UInt64> id;
protected readonly ContentCollection contentCollection;

protected ContentState(ContentCollection ownerCollection)
{
contentCollection = ownerCollection;
id = new ReactiveProperty<UInt64>();
}
}

public class EnemyState : ContentState
{
public readonly ReactiveProperty<UInt8> health;

public EnemyState(ContentCollection ownerCollection) : base(ownerCollection)
{
health = new ReactiveProperty<UInt8>();
}

public Enemy GetEnemy()
{
if (_contentCollection.enemies.TryGet(contentId, out Enemy enemy))
{
return enemy as Enemy;
}
return null;
}
}

public class GroundEnemyState : EnemyState
{
public GroundEnemy GetGroundEnemy()
{
if (_contentCollection.groundEnemies.TryGet(contentId, out GroundEnemy groundEnemy))
{
return groundEnemy as GroundEnemy;
}
return null;
}
}

public class WaterEnemyState : EnemyState
{
public readonly ReactiveProperty<bool> isStormResistant;

public WaterEnemyState(ContentCollection ownerCollection) : base(ownerCollection)
{
isStormResistant = new ReactiveProperty<bool>();
}

public WaterEnemy GetWaterEnemy()
{
if (_contentCollection.waterEnemies.TryGet(contentId, out WaterEnemy waterEnemy))
{
return waterEnemy as WaterEnemy;
}
return null;
}
}

Для сокращения рутинных операции, создание классов состояний можно вынести в зону ответственности кодогенератора.

Благодарю за внимание!
Вопросы в комментариях приветствуются!
Иерархия классов состояний
Друзья, приветствую 👋

Мы подошли к моменту, когда по плану публикаций, которого мы придерживаемся, осталось 2 поста:
- Плагины
- Сериализация и десериализация

На текущий момент мы выбираем дальнейший вектор развития 🔝и в дополнение к нашему видению нам очень нужна обратная связь от вас 🙏.


Поделитесь своими мыслями👇:

- чего именно вам не хватает на канале?
- чего вам не хватает глобально?
- требуется ли более глубокое раскрытие ранее рассмотренных тем?
- какие темы вам были бы наиболее интересны?


Ждем ваши предложения в комментариях и заранее благодарим за активность 🤝


Хороших выходных!
#разбор_тестового
Друзья, приветствуем 👋


Мы хотим добавить на канал разбор ваших тестовых и подготовили специальную форму для заявок 👉 https://forms.gle/kqVPv1jWT97Bkps9A


🚑 🚨 Основные показания - вы делали тестовое и, к сожаление, получили отказ без развернутой обратной связи. Либо развернутый отказ, с которым вы не согласны и хотите услышать альтернативное мнение, что не так.

ℹ️ На текущий момент частоту разборов не загадываем, начнем с одного тестового/месяц, чтобы не смещать фокус с основной линии группы.
📌 Форму запиним в описании канала. 📌


Хорошего дня 🤝
#архитектура #плагины #Unity3D

Приветствую, друзья
👋

Сегодняшняя тема посвящена плагинам в Unity 3D

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

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

Рассмотрим подробнее возможности плагинов в Unity 3D и особенности, которые необходимо учитывать при их использовании:

1️⃣Предоставление готовых решений:
Речь о возможности быстрой интеграции в проект Unity готовых решений и функциональности, реализованной сторонними разработчиками/компаниями.
Упрощение интеграции:
Как правило, плагины предоставляются в виде Unity Package (.unitypackage), поэтому их установка в проект Unity происходит через простой импорт и делает процесс интеграции быстрым и безболезненным.
Устанавлить плагины можно, как через Unity Package Manager (UPM), так и из сторонних источников.

2️⃣Решение сложных задач:
Часто решение сложных/трудоемких для реализации с нуля задач (например: системы аналитики, системы рекламной интеграцией, работа с физикой, и т.д.) упаковывают в плагины. Используя их, разработчики облегчают решение стоящих перед ними задач и получают проверенное, рабочее решение.

3️⃣Качество и поддержка:
В основном за разработку сторонних плагинов берутся опытные разработчики, они предлагают активную поддержку и обновления своих продуктов.
Это дает уверенность в качестве и надежности предоставляемых решений.

4️⃣Удобство управления платформенными зависимостями через External Dependency Manager (EDM4U) for Unity:
После установки плагина через Unity Package для управления платформенными (Android, iOS, и т.д.) зависимостями и обновлениями плагина можно использовать EDM4U ->
https://openupm.com/packages/com.google.external-dependency-manager/.
Это облегчает поддержку актуальных версий и упрощает процесс интеграции.
Как правило EDM4U входит в Unity Package плагина.

За разрешение платформенных зависимостей в EDM4U используется xml файл.
Данный файл должен находиться в директории плагина Editor и иметь имя *Dependencies.xml, где * означает имя директории плагина.

Внутри xml, как правило есть 2 секции:
- androidPackages. Зависимости, которые будут использоваться при Android сборке
- iosPods. Зависимости, которые будут использоваться при iOS сборке.

При конфликте (например при установки Pod'ов в iOS) потребуется модифицировать xml файл и подгонять его под нужные версии.
Под Android (Gradle) сборка, как правило проходит безболезненно.

‼️ℹ️Ниже приведены важные при работе с плагинами моменты, которые позволят избежать потенциальных проблем и сделать процесс разработки на Unity 3D более удобным и эффективным.

Важно:

- Следить за правильным подключением плагинов: все плагины необходимо устанавливать в папки по умолчанию (место будет предложено Unity при установке Unity Package).
Некоторые плагины "нацелены" на папки по умолчанию, и при размещении в альтернативном месте возрастает риск неопределенного поведения.

- Соблюдать версионность EDM4U: устанавливаемая версия должна соответствовать либо быть выше текущей версии, используемой в проекте

- Корректно размещать и именовать xml, отвечающий за решение платформенных зависимостей в EDM4U

- Тестировать работоспособность проекта после каждого изменения или добавления нового плагина.

Если в проекте требуется активно использовать предоставляемые плагинами API (например различные системы аналитики), я рекомендую выделить отдельный сервис/сервисы, которые будут использовать этот плагин.

Плагины продолжение 👇
#архитектура #плагины #Unity3D

Плагины продолжение


Слой сервисов и работа с плагинами

Напомню, слой сервисов разбирали в одной из предыдущих публикаций
Тезисно:
- Слой сервисов отвечает за набор дополнительных сервисов, которые потребуются например для сбора и анализа данных об игроках и их действиях в игре.
- Представляет собой набор классов с публичными методами.
- Сервисный класс должен предоставлять публичный доступ к API, который реализует сервис.
- Сервис создается один раз в нужном узле composition tree.
Далее в виде явной зависимости передаваться в контекстах вниз по иерархии composition tree.

ℹ️Например, если в проекте есть необходимость интегрировать несколько плагинов для работы с аналитикой: AppMetrica, Firebase, Amplitude, Appsflyer, Adjust, Tenjin, и т.д.
Итоговый сервис должен представлять собой один класс, который в конструкторе инстанцирует, инициализирует и дожидается (может происходить асинхронно) готовности каждой системы и имеет набор нужным публичных методов для работы с аналитикой.

При создании инстанса класса используется контекст (мы писали тут), в который можно передать нужные ключи/токены для инициализации плагинов.

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

public void Track(string eventName, Dictionary<string, object> data)
{
...
}

В коде из контекста, вы всегда сможете получить доступ к AnalyticService и вызвать метод Track с нужными параметрами.

Резюмирую все вышесказанное
Сторонние плагины для Unity 3D, совместно с UPM и EDM4U дают значительные преимущества для разработчиков 👇 :
- расширяют возможности Unity, предоставляя готовые решения для сложных задач, таких как интеграция с внешними сервисами, взаимодействие с социальными сетями, поддержка анимации, управление физикой и многое другое.
- облегчают обносления: используя UPM и EDM4U, разработчики могут легко обновлять плагины до последних версий семантического управления версиями.
- гарантируют качество и поддержку: сторонние плагины находятся на поддержке и обновлении у разработчиков. Это гарантирует качество и надежность решений.
- ускоряют разработку: позволяют сэкономить время и ускорить процесс создания игр и приложений в Unity 3D.
- упрощают процесс разработки и повышают понимание кода через использование сервисного слоя в composition tree проекта.

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

Благодарю за внимание!
Вопросы в комментариях приветствуются!
#unity_3d #prefab #модель #анимация #fbx

Приветствую 👋

Пока я готовлю материал по десереализации, предлагаю поучаствовать в дискуссии.

Давайте обсудим проблему на стыке взаимодействия нескольких отделов игровой студии: аниматор+3d художник -> разработчик.

В играх с 3d персонажами перед командой стоит задача частого преображения/изменения героя: 3d модели, анимаций или стилизация (например: смена одежды).

Зоны ответственности поделены между:
- аниматор+3d художник - результатом работы которых является fbx файл
- разработчик создает и модифицирует prefab, созданный из fbx. В иерархии prefab присутствует набор скриптов.

Таким образом при модификации арт командой исходного fbx, разработке необходимо применить изменения для prefab.

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

Документация Unity достаточно скудно покрывает данный вопрос и не содержит коробочных решений!

Предлагаю обсудить:
- сталкивались ли вы с данной проблемой на практике и как решали?
- если не сталкивались, предположите порядок действия для изменения корректного переноса изменений в prefab.

Хорошего дня!
Хороших выходных!
#сериализация #десериализация #Unity3D

Приветствую, друзья 👋
Сегодня говорим про сериализацию и десериализацию.

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

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

Итак 📝

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

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

В каких случаях необходимо сохранение и восстановление данных?

➡️ Поддержание возможности завершения и продолжения игры в любой момент: сериализация позволяет сохранять прогресс игры, состояния персонажей и другие важные параметры игры.

➡️ Синхронизация в сетевых и многопользовательских проектах:
Для данной категории проектов сериализация и десериализация позволяют синхронизировать состояния игрового мира между разными участниками и управлять передачей данных по сети.

➡️ Сохранение пользовательских настроек и ресурсов:
Сериализация позволяет сохранять пользовательские настройки, конфигурации уровней и другие ресурсы в удобном формате.

‼️ Сериализация/десериализация способна оказывать негативное влияние на производительность.

Рассмотрим методы, позволяющие разработчикам управлять процессом сериализации и десериализации с минимальными негативными воздействиями на производительность:
1. Использование пула объектов.
Огромный пласт и отдельная тема. Подобный подход используют разработчики уровня Senior+.

2. Использование Newtonsoft.Json (JsonNET).
Данный подход имеет низкий входной порог и широко применяется в проектах на Unity3D для управления сериализацией и десериализацией.

Именно по этой причине данный подход будет разобран в рамках публикации.
Поехали 🚘

JsonNET широко используется в различных проектах на C#, включая веб-приложения, приложения для мобильных устройств, сервисы и т. д.
Библиотека является одной из наиболее популярных и надежных для работы с JSON в .NET-приложениях.
Предоставляет мощные инструменты для настройки процесса преобразования данных в формат JSON и обратно.
Позволяет гибко управлять иерархической структурой объектов, обрабатывать специфичные типы данных и применять различные стратегии именования.

Основные возможности и характеристики JsonNET:
➡️ Сериализация и десериализация:
Позволяет легко сериализовать .NET-объекты в JSON и десериализовать JSON в .NET-объекты.

➡️ Гибкая настройка:
Предоставляет множество параметров и настроек для контроля процесса сериализации и десериализации, включая атрибуты и альтернативные имена полей.

➡️ Интеграция с LINQ (Language Integrated Query):
Позволяет выполнять запросы и манипулировать данными JSON как структурированными.

➡️ Поддержка динамических данных:
Предоставляет поддержку динамических объектов для работы с неизвестными заранее структурами данных.

➡️ Обработка специальных типов:
Умеет работать с различными специальными типами данных: даты, перечислениями и байтовыми массивами.

➡️ Пользовательские конвертеры:
Возможность определить свои собственные конвертеры для специфических типов данных и управлять процессом сериализации и десериализации.

➡️ Поддержка атрибутов:
Поддерживает атрибуты для более точного управления сериализацией и десериализацией.

➡️ Поддержка JSON Schema:
Позволяет валидировать и генерировать схемы на основе объектов.

Отдельной затрону конвертеры в JsonNET

Хорошего дня!
#сериализация #десериализация

Приветствую
👋
Сегодня говорим про конвертеры в JsonNet. Первая часть статьи про сериализацию и десериализацию тут

Конвертеры в JsonNET представляют собой механизм для настройки пользовательской сериализации и десериализации определенных типов данных или объектов в формате JSON. Дают возможность настроить процесс преобразования объектов в JSON и обратно.

JsonNet предоставляет два основных типа конвертеров:
- JsonConverter;
- JsonConverter<T>,
где T - это конкретный тип данных, для которого конвертер применяется.

За определение логики сериализации и десериализации отвечают методы WriteJson и ReadJson, затронем их ниже по тексту.

Как работают конвертеры:

Наследование - создание собственных классов наследованием от JsonConverter или JsonConverter<T>.

Сериализация:
В методе WriteJson определяем преобразование объекта в JSON.
Создаем JSON объекты, устанавливаем свойства и вызываем методы для добавления данных в JSON строку.

Десериализация:
В методе ReadJson определяем преобразование JSON обратно в объект.
Читаем свойства из JSON строки, создаем и инициализируем объекты, затем возвращаем получившийся объект.

Рассмотрим пример простого конвертера для преобразования строки в верхний регистр:

В этом примере UpperCaseConverter преобразует строку в верхний регистр как при сериализации, так и при десериализации.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;

public class UpperCaseConverter : JsonConverter<string>
{
public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer)
{
writer.WriteValue(value.ToUpper());
}

public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;

return reader.Value.ToString().ToUpper();
}
}
Применение этого конвертера:

class Program
{
static void Main()
{
var settings = new JsonSerializerSettings
{
Converters = { new UpperCaseConverter() }
};

string json = JsonConvert.SerializeObject("hello", settings);
Console.WriteLine(json); // Output: "HELLO"

string value = JsonConvert.DeserializeObject<string>(json, settings);
Console.WriteLine(value); // Output: "HELLO"
}
}


Настройка единой конфигурации сериализации и десериализации для всего проекта.
Класс JsonSerializerSettings позволяет настроить единую конфигурацию сериализации и десериализации для всего проекта и управлять процессом из единого места.

Разберем подробнее
Настроим ContractResolver и NamingStrategy с помощью JsonSerializerSettings.
Contract Resolver (ContractResolver) - определяет, как JsonNet будет находить и настраивать контракты для сериализации и десериализации. Позволяет контролировать, какие свойства будут включены в процесс, игнорировать некоторые свойства, управлять настройками атрибутов и т. д.

Naming Strategy (NamingStrategy) - позволяет управлять форматированием имен свойств в JSON, определяет, как будут преобразованы имена свойств объектов в имена полей JSON и наоборот.

Рассмотрим пример использования JsonSerializerSettings:

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

class Program
{
static void Main()
{
var settings = new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy() // Use CamelCase naming strategy
},
Formatting = Formatting.Indented // Optional formatting for readability
};

var data = new { UserName = "Sergey", UserAge = 40 };

string json = JsonConvert.SerializeObject(data, settings);
Console.WriteLine(json);
}
}


В примере ContractResolver устанавливается в DefaultContractResolver, а NamingStrategy в CamelCaseNamingStrategy, что преобразует имена свойств в стиле Camel Case.

Хорошего дня!
#сериализация #десериализация

Приветствую 👋

Сегодня говорим про применение конвертеров для реактивных свойств.

Данная публикация третья, предыдущие по ссылкам:
часть 1
часть 2

Конвертеры в JsonNET можно применять как для обычных типов данных, так и для более сложных структур - реактивных свойств (например, ReactiveProperty), в ситуации когда необходимо корректно выполнять JSON сериализацию и десериализацию объектов, содержащих реактивные свойства.

Использование конвертеров позволяет решить проблему корректной обработки такого типа объектов, поскольку стандартные механизмы JSON сериализации не всегда способны сделать это корректно.

Пример создания конвертера для ReactiveProperty:

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Reactive.Subjects;

public class ReactivePropertyConverter<T> : JsonConverter<ReactiveProperty<T>>
{
public override void WriteJson(JsonWriter writer, ReactiveProperty<T> value, JsonSerializer serializer)
{
if (value != null)
{
serializer.Serialize(writer, value.Value);
}
else
{
writer.WriteNull();
}
}

public override ReactiveProperty<T> ReadJson(JsonReader reader, Type objectType, ReactiveProperty<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}

T innerValue = serializer.Deserialize<T>(reader);
return new ReactiveProperty<T>(innerValue);
}
}


Рассмотрим применение написанного выше конвертера для сериализации и десериализации ReactiveProperty:

class Program
{
static void Main()
{
var settings = new JsonSerializerSettings
{
Converters = { new ReactivePropertyConverter<int>() }
};

ReactiveProperty<int> reactiveValue = new ReactiveProperty<int>(42);

string json = JsonConvert.SerializeObject(reactiveValue, settings);
Console.WriteLine(json); // Output: 42

ReactiveProperty<int> deserializedValue = JsonConvert.DeserializeObject<ReactiveProperty<int>>(json, settings);
Console.WriteLine(deserializedValue.Value); // Output: 42
}
}


В примере выше ReactivePropertyConverter позволяет корректно выполнять JSON сериализацию и десериализацию ReactiveProperty<int> объектов.
Это дает возможность сохранять и восстанавливать состояние реактивных свойств при работе с JSON форматом.

JsonNET позволяет выполнять процесс сериализации и десериализации JSON без использования дополнительных атрибутов в классах. Что особенно удобно, если у вас нет возможности или желания модифицировать существующие классы добавлением атрибутов.

Вместо атрибутов можно использовать JsonSerializerSettings, ContractResolver и NamingStrategy (писал ранее). Они позволят контролировать процесс сериализации и десериализации без внесения изменений в классы данных.

Пример использования JsonSerializerSettings для сериализации и десериализации без атрибутов.

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;

class Program
{
static void Main()
{
var settings = new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy()
},
Formatting = Formatting.Indented
};

// Serialize
var data = new { UserName = "Sergey", UserAge = 40 };
string json = JsonConvert.SerializeObject(data, settings);
Console.WriteLine(json);

// Deserialize
var deserializedData = JsonConvert.DeserializeObject(json, settings);
Console.WriteLine(deserializedData);
}
}


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


Хорошего дня!
#задача #unity
Приветствую 👋

Предлагаю разобрать задачу, которая легко может ввести в ступор разработчика 🫣

Итак, рассмотрим пример из проекта 👇
У нас в игре есть турель (GameObject), которая стреляет по некоторому таргету (Transform).
Дуло пушки (находится внутри GameObject), поднято на некоторый оффсет (x,y,z).

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

Напишите в комментариях, как бы вы решали данную задачу ✍️
Ближе к выходным расскажу вам свое решение и дам комментарий по вашим подходам!

Хорошего дня 🤝
Приветствую 👋

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

Напомню вводные В игре есть турель (GameObject), которая стреляет по некоторому таргету (Transform).
Дуло пушки (находится внутри GameObject), поднято на некоторый оффсет (x,y,z).
При стрельбе пушка должна поворачиваться к таргету так, чтобы дуло смотрело точно в центр таргета.


Мой вариант строится на векторной алгебре.

Для его рассмотрения обратимся к приложенной иллюстрации.

Как видно из рисунка, для поворота пушки в положение, когда ее дуло нацелено на таргет, необходимо, чтобы форвард пушки был параллелен векторной разнице (target.position - muzzle.position).
Результатом разницы будет вектор, начало которого лежит в центре дула, конец - в центре таргета.

Код:
transform.rotation = Quaternion.LookRotation(target.position - muzzle.position);


Хороших выходных 🤝
#Unity3D

Друзья, приветствую👋

Предлагаю запустить цикл вопрос/ответ!

Жду ваши вопросы в комментариях!
В рамках недели буду порционно отвечать на каждые 5-10 в зависимости от объема.

Хорошего дня.
#вопрос_ответ

Приветствую
👋
Сегодня разберу один из недавно заданных вопросов. Ответ получился объемным, на публикацию 📚

Вопрос от @PavelAltynnikov
В игре есть инвентарь, в котором лежат такие типы предметов:
- потребляемые, используемые и неиспользуемые.

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

Как бы вы решили проблему связи предметов инвентаря с характеристиками игрока?

Приступим
В ответе обращаюсь к слоям абстракции, также использую термин “провода” и реактивные объекты. Рассказывал в “Слои абстракции” и “Связи в проекте”.

Также к “проводам” отношу все динамические поля, которые меняются в процессе игры. Ответ упрощен.
Считаю, что вы знакомы (хотя бы обзорно) с теми архитектурными подходами, которые я рассматривал в цикле статей.

В проекте необходимо сделать деление на слои абстракции.

Предметы с исходными характеристиками - слой контента.
Условно иерархия контентных классов ⬇️ :
- Content, внутри класса есть поле id

- Item - наследник Content
- ConsumableItem - наследник Item
- UsableItem - наследник Item
- …
‼️Для упрощения считаю, что классы выше уже созданы

- GameEffect - наследник Content
- GameEffectInstant - наследник GameEffect
- GameEffectMana - наследник GameEffectInstant. Содержит поле mana, значение увеличения/уменьшения маны
- GameEffectPersistent - наследник GameEffect
- GameEffectDexterityModifier - наследник GameEffectPersistent. Содержит поле dexterityIncrement, значение увеличения/уменьшения ловкости
- GameEffectPoisonResistanceModifier - наследник GameEffectPersistent. Содержит поле poisonResistanceIncrement, значение увеличения/уменьшения устойчивости к яду
- GameEffectWisdomModifier - наследник GameEffectPersistent. Содержит поле wisdomIncrement, значение увеличения/уменьшения мудрости

- GameEffectApplicator - наследник Content. Содержит поля:
- trigger: onUseInventoryItem (при использовании), onWearInventoryItem (при одевании), onItemAddToInventory (при добавлении в инвентарь)
- item (инстанс контентного класса Item)
- gameEffect (инстанс контентного класса GameEffect, какой эффект применить).
‼️Инстанс класса является связующим звеном между событием в игре и эффектом, который нужно применить.

"эликсир маны" - инстанс UsableItem.
"рукавицы из шкуры дракона" - инстанс ConsumableItem.
"тотем" - инстанс Item.

Перейдем к иерархии классов состояний, слою состояний/State
- State содержит поля: id, contentId

- ItemState - наследник State
- ConsumableItemState - наследник ItemState
- UsableItemState - наследник ItemState
- …
‼️ Для упрощения считаю, что классы выше уже созданы

- GameEffectState - наследник ItemState
- GameEffectPersistentState - наследник GameEffectState
- GameEffectDexterityModifierState - наследник GameEffectPersistentState
- GameEffectPoisonResistanceModifierState - наследник GameEffectPersistentState
- GameEffectWisdomModifierState - наследник GameEffectPersistentState.

Профиль - состояние игрока, набор динамических параметров:
- mana - текущее значение маны
- dexterity - текущее значение ловкости
- poisonResistence - текущее значение устойчивости к яду
- wisdom - текущее значение мудрости
- inventory - инвентарь
- gameEffectStates - состояния игровых эффектов.

В профиле могут присутствовать и другие состояния.

Инвентарь - это часть состояния игрока, которое находится в профиле.

Продолжение в комментариях ⬇️
#вопрос_ответ

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

Вопрос звучит так 👇
Хотелось бы узнать,кто как бы решил такую ситуацию:
Есть персонаж, на нем около 30-40 коллайдеров (руки,плечи,ноги и т.д.)
и нужно ловить на нём коллизии и вызывать класс, который лежит на самом верхнем слое персонажа. Кто как решал бы данную проблему?
а)Использовали б поиск скрипта с поиском по родителям?(если часто вызываться будет и много вложений, проблемы с производительностью возможны).
б)Использовать какие-нибудь collision handlers,которые будут вызывать события,а в верхнем персонажи будет какой-то скрипт,который будет на них подписываться? (не много ли подписок будет?)

Ответ 👇
Приведу пример нанесения урона персонажу, который может содержать несколько зон получения урона.

Я бы делал так:
- в ветке composition tree, которая относится к уровню (пусть это будет LevelEntity) создаем рычаги:
- onDealDamage (содержит информацию о коллайдере и характеристиках атаки)
- receiveDamage (содержит информацию о том куда именно было попадание State того, кому наносим урон, характеристики атаки и зоне попадания)
- в Pm, которая отвечает за физику нанесения урона, например EnemyAttackPm мы получаем набор коллайдеров, с которыми было пересечение, вызываем onDealDamage (столько раз, сколько было задето коллайдеров), который прокидывается вниз по иерархии от LevelEntity
- создаем View DamageReceiverView (содержит поле collider и какой-то идентификатор зоны: enum), которая принимает в контексте State персонажа, onDealDamage и receiveDamage.
В методе установки контекста подписываемся на onDealDamage с фильтрацией коллайдера.

В методе подписки вызываем receiveDamage с нужными параметрами: State кого атакуют, характеристиках атаки и зоне.
- в иерархии персонажа кидаем в нужные GameObject DamageReceiverView (таких мест будет столько, сколько зон попадания у вас есть), ставим в них нужные коллайдеры. Ставим идентификатор.
Во View персонажа (содержит поля с DamageReceiverView, которые именуются по зонам попадания) перетаскиваем нужные DamageReceiverView.
В контексте View персонажа заполняем контексты DamageReceiverView.
- также в иерархии composition tree должен быть DamageSystemPm, в который приходит рычаг receiveDamage, с информацией о State кого атакуют и информацией об атаке и зоне. Эта Pm'ка принимает решения и отвечает за изменения State того, кого атакуют.

По поводу подписок, если речь про UniRx, то с ними всё будет хорошо даже, если их тысячи в проекте. Главное не забывать делать Dispose подписке, когда она больше не нужна.

Нужно помнить, что явное всегда лучше неявного.

Когда вы используете GetComponentInParent (или что-то аналогичное), то это означает что у вас в Design time еще не выстроена связь и непонятно, выстроится ли в Runtime (разработчик надеятся, что скрипт будет найден, но по факту может и не быть найден).

Если вы делаете прототип и быстро нужно получить MVP, то нужно выбирать самый быстрый вариант.

Хорошего дня!
Приветствую 👋 Мне понравилось, как прошел цикл вопрос-ответ 🤝. Если данный формат полезен, предлагаю продолжить. Оставляйте ваши вопросы и предложения по темам в комментариях. Начнем?
Anonymous Poll
97%
Да
3%
Нет