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

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

Приветствую, друзья 👋
В рамках опроса про async метод, возник вопрос про асинхронность.
В данной публикации постараюсь развернуто ответить на заданный вопрос.


Поехали 🚀

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

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

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

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

В языке программирования C# и фреймворке Unity асинхронное программирование можно реализовать с помощью ключевых слов async и await, а также с помощью использования корутин.
Эти механизмы позволяют реализовать асинхронный код более простым способом и повысить его читаемость, избегая явного управления потоками и блокировок.

В ходе дискуссии под опросом один из участников высказал предположение 👇:

Где не найду описание, везде написано что поток может смениться после ожидания асинхронной операции.
Т.е. первая операция которая выполняется через await всё таки выполнится на главном потоке и только после этого может оказаться что код выполняется другим потоком?
...
//главный поток
await UniTask.WaitUntil(() => image.Source == icon).ConfigureAwait(false); //главный поток
//неизвестный поток(в том числе главный)
image.Source = icon; //возможна ошибка, если не главный поток
...


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

Условия, при которых возможно применение await для типа:
- тип должен иметь public (или internal) метод GetAwaiter(), который должен вернуть тип ожидаемого объекта,
- ожидаемый объект должен реализовать интерфейс INotifyCompletion.

UniTask не содержит метод ConfigureAwait. ConfigureAwait содержит обычный Task и при его await можно сделать ConfigureAwait.
С UniTask достаточно сложно допустить ошибку с продолжением на разных потоках, так как после его await управление остается на главном потоке. В этом есть свои минусы, о которых я расскажу позже.

Что касается корутин.

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

Корутины позволяют:
- делать паузы в выполнении кода,
- возвращать промежуточные результаты,
- выполнять итерации по определенным шагам.

Рассмотрим пример работы с корутинами в Unity 👇:

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

IEnumerator MyCoroutine()
{
// Шаг 1
Debug.Log("Шаг 1");
yield return new WaitForSeconds(1f); // Пауза на 1 секунду

// Шаг 2
Debug.Log("Шаг 2");
yield return new WaitForSeconds(0.5f); // Пауза на 0.5 секунды

// Шаг 3
Debug.Log("Шаг 3");
yield return null; // Пауза на один кадр

// Шаг 4
Debug.Log("Шаг 4");
}
Вызовем корутину с помощью функции StartCoroutine в методе Start.


Продолжение в комментариях👇
Там же ждем ваши вопросы.
Всем хорошего дня👍
#результаты_опроса

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


Мы остановили опрос и спешим подвести итоги!
Напомним суть 👇
Есть async метод, в нем выполняется долгая операция, не связанная с объектами Unity. В методе нет асинхронных вызовов и кода для явного запуска новых тредов. Что произойдет при await метода?

Правильный ответ - синхронно будет выполнен код метода

Обо всем по порядку, начнем со статистики.
В опросе приняли участие 105 человек, из них:

18% - метод выполнится на главном треде, но чуть позже
20% - метод будет обработан отдельным тредом из тред пула
19% - затруднились с ответом
43% - синхронно будет выполнен код метода 👏 Наши поздравления 🤝


Рассмотрим подробнее, что же происходит
👇
Если в C# вызвать асинхронный метод без использования оператора await, код будет выполнен синхронно.
При вызове асинхронного метода, который возвращает Task, без использования оператора await, метод будет выполняться последовательно и блокировать вызывающий поток до завершения метода.
Это происходит, потому что отсутствие оператора await отменяет асинхронное поведение, и метод ведет себя как обычный синхронный метод.

Пример 1:

public async Task DoSomethingAsync()
{
await Task.Delay(1000);
Console.WriteLine("Async method executed.");
}

public void CallAsyncMethod()

{
// Код будет выполнен синхронно
DoSomethingAsync();
Console.WriteLine("CallAsyncMethod completed.");
}


ℹ️Если в примере выше вызвать CallAsyncMethod, метод DoSomethingAsync будет выполнен синхронно, и "Async method executed." будет выведено только после того, как задержка в Task.Delay(1000) и завершение DoSomethingAsync будут завершены.
Затем будет выведено "CallAsyncMethod completed.".

ℹ️Чтобы получить асинхронное поведение и дождаться завершения асинхронного метода, необходимо использовать оператор await.


ℹ️ Если в рассмотренном выше примере (Пример 1) добавить оператор await при вызове DoSomethingAsync, код будет ожидать завершения метода перед переходом к следующей инструкции:

public void CallAsyncMethod()
{
// Код будет ждать завершения асинхронного метода
await DoSomethingAsync();
Console.WriteLine("CallAsyncMethod completed.");
}


Теперь "Async method executed." будет выведено перед "CallAsyncMethod completed." и выполнение CallAsyncMethod будет приостановлено, пока DoSomethingAsync не завершится.

Благодарим за участие 🤝
Хорошего дня!
#абстракции #чистый_код #полиморфизм

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

Сегодняшняя публикация будет посвящена абстракции.

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

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

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

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

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

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

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

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

Продолжение 👇
#абстракции #чистый_код #полиморфизм

Абстракции продолжение 👇


Рассмотрим подход с использованием runtime полиморфизма отдельного метода и реактивного программирования в контексте Unity и C#.

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

Необходимо помнить:

- если в ReactiveProperty дважды и более раз будут приходить одинаковые значения, подписчик сработает только 1 раз
- если в ReactiveProperty было присвоено Value до подписки на ReactiveProperty, то после подписки сработает обработчик подписки
- обработчик подписки на ReactiveCommand будет срабатывать каждый раз при вызове команды
- обработчик подписки на ReactiveCommand не сработает, если подписка произойдет после вызова ReactiveCommand.

Знание данных особенностей позволяет разработчику сформировать понимание, в каких случаях оптимально использовать ReactiveProperty и ReactiveCommand.

Применение ReactiveProperty и ReactiveCommand позволяет создавать полиморфизм на уровне отдельных методов, через передачу методов или изменяемых значений как параметров.

В рассматриваемом подходе все зависимости передаются через специальные структуры, которые мы будем называть Ctx (данное имя вы встречали ранее, как в примерах кода, так и статьях, Ctx - сокращение от "context" - контекст).

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

Напомню, что в предлагаемом мной подходе к проектированию архитектуры, я минимально выделяю семь слоев(подробнее говорили про слои по ссылке https://t.me/gamedev_unity3d/17):
- слой бизнес-логики
- слой представления
- слой связей
- слой жизненного цикла объектов
- слой контента (данные инстанса должны быть неизменны на протяжении жизни приложения)
- слой состояния игровых объектов
- слой сервисов.

В каждом слое есть набор классов, у каждого класса внутри есть публичная структура - контекст, которая принимается в виде зависимости в конструкторе (если это не MonoBehaviour класс) или в отдельном методе SetCtx (для MonoBehaviour класса).

Зависимости, которые допускаются в контексте:
- контентные классы (в том числе коллекции)
- сервисные классы
- классы состояния игровых объектов
- реактивные объекты.

Продолжение 👇
#абстракции #чистый_код #полиморфизм

Абстракции продолжение
👇

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

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

Благодарим за внимание!
Хороших выходных 😉
Приветствую, друзья! Хотим изменить структуру опроса. Важно ваше мнение! Суть - сделать опрос-дискуссию. Мы задаем вопрос с вариантами, например: не знаю/отвечу в комментариях! Выбравший “отвечу в комментариях” пишет свой ответ. Как вам идея? Пробуем?
Anonymous Poll
73%
Да 👍
25%
Нет 👎
2%
Своя идея в комментариях
#опрос #async #gamedev

Итак, друзья )

Тестируем наш первый опрос в новом формате (спасибо за идею механики Алексею 😊)

Есть код, эммулирующий загрузку данных из сети 👇

TimeSpan workDuration = TimeSpan.FromSeconds(10);
DateTime endDateTime = DateTime.Now.Add(workDuration);

while (DateTime.Now < endDateTime)
{
// some fast network operation, receive data from socket, speed 200 Mbps
Task receiveDataTask = Task.Delay(1);
//
await receiveDataTask;
}

 
Все ли в порядке с кодом? Или код требует улучшения

ГОЛОСОВАТЬ >>>

Внимание Обсуждаем код и ведем дискуссию здесь, а в следующем посте Вы только голосуете и расписываете ответ в комментариях, если считаете, что код требует улучшений
Требует ли улучшений код выше 👆. Если вы считаете, что код отработает некорректно, опишите риски здесь в комментариях. Если уже есть мнение, совпадающее с вашим, лайкнете его 👍
Final Results
64%
не знаю/затрудняюсь с ответом
15%
код отработает корректно
21%
вижу риски (напишу в комментариях)
#unity #асинхронность #производительность
Приветствую, друзья 👋

Подведем итоги по проблеме с кодом, который разбирали в недавнем опросе 👇

Пример 1.
TimeSpan workDuration = TimeSpan.FromSeconds(10);
DateTime endDateTime = DateTime.Now.Add(workDuration);

int iterationsCount = 0;
while (DateTime.Now < endDateTime)
{
// some fast network operation, receive data from socket, speed 200 Mbps
Task receiveDataTask = Task.Delay(1);

await receiveDataTask;

iterationsCount++;
}

Debug.Log($"Iterations count: {iterationsCount}");


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

Получение данных из сокета происходит в отдельном треде (Task.Delay(1)).
Так, как операция выполняется быстро, то await на каждой итерации необходимо будет ждать, пока главный поток освободится.
Следовательно, в данном случае узкое горлышко - главный поток.

Предлагаю рассмотреть разницу между кодом из опроса (Пример 1) и приведенным ниже 👇

Пример 2.
TimeSpan workDuration = TimeSpan.FromSeconds(10);
DateTime endDateTime = DateTime.Now.Add(workDuration);

int iterationsCount = 0;
while (DateTime.Now < endDateTime)
{
// some fast network operation, receive data from socket, speed 200 Mbps
Task receiveDataTask = Task.Delay(1);

await receiveDataTask.ConfigureAwait(false);

iterationsCount++;
}

Debug.Log($"Iterations count: {iterationsCount}");


Пример 1 и Пример 2 будут сильно отличаться по количеству итераций.
Бóльшее количество итераций будет в примере 2.
Всё зависит от того, как долго у нас занят главный поток на каждой своей итерации. Если время получения данных из сокета на каждой итерации (t1) в нашем цикле меньше времени одной итерации главного потока (t2), то мы при await receiveDataTask упрёмся во время t2 и будем ожидать ~ t2.

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

Пример 2
показывает, как можно избежать просадки производительности, используя ConfigureAwait(false) для receiveDataTask. В таком случае продолжение будет выполнено вне главного потока и ожидания не будет.

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

Хорошей всем недели 💪.
#архитектура #связи

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

Сегодня поговорим про связи в проекте.
Публикацию разобью на две части:
- Часть 1 Общее представление о связях и их реализации
- Часть 2 Мой подход к организации связей.

Итак, поехали 🚀
Выстраивание слоя связей - один из важнейших аспектов игровой разработки.

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

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

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

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

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

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

3️⃣ Расширяемость:

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

4️⃣ Тестируемость:
Облегчает модульное тестирование компонентов системы.
Каждый компонент может быть протестирован независимо, что обеспечивает более надежное и эффективное тестирование системы в целом.

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

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

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

Слой связей часто реализуется с использованием паттернов проектирования, таких как "Наблюдатель" (Observer), "Адаптер" (Adapter) и паттерн "Фасад" (Facade).

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

Продолжим завтра

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

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

Сегодня продолжаем про слой связей!
Рассмотрим мой подход к организации слоя связей, для большей наглядности и понимания организации я подготовил иллюстрацию “Связь между классами”.

Приступим 🚀

В предлагаемом мной подходе в слое связей я выделяю 3 основные роли:

1️⃣ Первая роль – инициатор действия/изменения данных.
Этот участник запускает процесс обновления данных самостоятельно либо в ответ на какое-либо событие или внешний запрос.
Он передает данные через установленные связи, что инициирует реакцию обработчика и может привести к изменениям в системе.

2️⃣ Вторая роль – точка стыковки зависимости. Участник, который создает связи и определяет структуру передачи данных.

Ответственность участника:
- задать способы соединения различных компонентов системы
- установить правила передачи данных от одного узла к другому.

3️⃣ Третья роль – обработчик действия. 
Участник, который использует созданные ранее связи и подписывается на изменения данных внутри объекта.
Может быть реализован в виде обработчика, который реагирует на изменения данных, полученные через связи, и выполняет соответствующие действия.
Участник использует информацию от создателя связей для обновления состояния или выполнения определенных операций.

ℹ️ Разберем подробнее схему “Связи между классами”.

Как правило все основные связи создаются в Entity.
Связь реализуется через контексты.
В контексты передаются обычные классы или реактивные объекты: ReactiveProperty и ReactiveCommand.
PM, View, Service могут использовать, подписываться или изменять данные внутри контекста.

В узел PlayerEnity нашего Composition Tree в контексте приходит зависимость A.
Данную зависимость мы можем использовать для всех объектов, которые порождает PlayerEnity.

Примером такой зависимости может быть реактивный объект ReactiveProperty<bool> isAutopilotEnabled, который отражает состояние включен автопилот в игре или нет.

PlayerEnity создает зависимость B (приватное поле класса), например ReactiveProperty<Vector3> _lookTransform, которая отражает вектор, куда сейчас смотрит игрок.
Аналогично с остальными зависимостями.

PlayerEnity порождает PlayerView и PlayerPm.
В PlayerPm в контексте передается зависимость B, данные которой PlayerPm будет изменять.
В PlayerView в контексте также передается зависимость B, на которую PlayerView будет подписываться.
Обрабатывать подписку будет логика в приватном методе класса.

Связь устанавливается тогда, когда выстроена цепочка: инициатор (PlayerPm), точка стыковки (создание _lookTransform и передача _lookTransform в контекст PlayerPm, PlayerView), подписчик (подписка в PlayerView, которую обрабатывает приватный метод класса).

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

Хорошего дня 👍
Связь.png
53.4 KB
Схема “Связи между классами”
#опрос

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

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

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


Задача оценить оптимальность описанного подхода и рассказать о своем, если он отличена от приведенного выше. Если ваш подход совпадает с описанным - раскрыть, почему с вашей точки зрения данная реализация оптимальна

ГОЛОСОВАТЬ >>>

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

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


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

Вопрос звучал:
Есть 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;
}
}

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

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