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

Заявка на разбор тестовых https://forms.gle/kqVPv1jWT97Bkps9A
Download Telegram
Приветствую, друзья 👋

Вчера состоялось замечательное событие Yet Another Mobile Party или YAMP!
Было множество интересных докладчиков и докладов про мобильную разработку

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

Полный выпуск по ссылке https://www.youtube.com/watch?v=dEKN4NYPs3M

Устраивайтесь поудобнее и приятного просмотра
#разбор_кода #архитектура #конкурс

🔴 СТОП 🔴


За выходные у нас родилась идея провести розыгрыш книги: “Паттерны объектно-ориентированного проектирования, Влиссидес Джон, Хелм Ричард “. *

Для участия в розыгрыше необходимо выполнить задание 👇:
- Реализовать нанесение урона игроком врагу (делаем с одним врагом, но в голове держим, что их может быть много).
- Предусмотреть несколько зон попадания: голова, руки, торс, ноги.
- Попадание в каждую зону предусматривает разный урон.
- Максимальный HP врага - 100%.

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

Срок неделя, т.е. код ждем до 10.04 (следующий понедельник) до 12:00 по Москве).
Если по результатам выполнения, будет несколько претендентов на победу, победитель будет определен путем голосования в группе.

ℹ️ Желающие принять участие, плюсаните в комментарии либо в личку Ирине (t.me/IrinaZhi)
И конечно же все, принявшие участие, получат обратную связь.

Хорошего дня и продуктивной рабочей недели 💪

*Доставка силами Ozon.
Приветствую 👋 Проверим знания или интуицию 😉 ➡️ Есть класс, одно из полей - структура. В конструкторе класса инициализируем структуру. Где будет находится структура после выхода из конструктора: на стеке или в куче? Будет ли при этом упаковка?
Final Results
23%
В куче, будет упаковка
34%
В куче, упаковки не будет
9%
На стеке, будет упаковка
24%
На стеке, упаковки не будет
10%
Пишите статьи, не задавайте вопросы
Приветствую 👋

Подведём итоги опроса -> Есть класс, одно из полей - структура. В конструкторе класса инициализируем структуру. Где будет находится структура после выхода из конструктора: на стеке или в куче? Будет ли при этом упаковка?

Верный ответ (34% опрошенных) -> Структура будет находиться в куче, упаковки не будет

Но обо всем по порядку, давайте рассмотрим как распределились голоса 👇

В опросе приняли участие 119 человек:
33% считают, что после выхода из конструктора структура будет находится на стеке,
57% считают, что после выхода из конструктора структура будет находиться в куче.
Из выбравших вариант в куче
- 23% (от 119 голосов) - считают, что упаковка будет
- 34% (от 119 голосов)💪 - упаковки не будет

Как это работает и что происходит со структурой 👇
1. При выделении памяти под новый инстанс класса одним из детерминированных значений будет размер, требуемый на хранение данных этого класса. В это значение будет включена и структура, ее размер в памяти будет заранее известен.
2. Класс - это ссылочный тип, он будет инстанирован в куче. При выделении памяти в куче будет зарезервировано значение под структуру.
3. В конструкторе класса при создании и инициализации структуры, до момента из выхода из конструктора структура находится на стеке. Во время присвоения созданной структуры в поле класса происходит её копирование в кучу.
4. Никакой упаковки при этом не будет.


Благодарим за активность 👍
Всем хорошего дня 💪
#результаты

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

Не так давно мы проводили конкурс, с призом - книга "Паттерны объектно-ориентированного проектирования", Влиссидес Джон, Хелм Ричард.

Спасибо всем участникам, победитель определен, им стал Артур Зайцев👏👏👏.

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

Мы поздравляем Артура, желаем непрерывного роста и развития, ведь нет предела совершенству! И конечно же интересных и масштабных проектов 🥳💪

Вот, что Артур рассказал о себе 👇
В геймдеве я уже 6 лет, и только сейчас добрался до того, чтобы нормально засесть за пет проект. Начинал с VR проектов для бизнеса, потом был долгий период мобилок, и сейчас вернулся в VR, но уже делаю ПК игры.

Всем хорошего дня и продуктивной недели 💪
#архитектура #dependency_injection #di #zenject

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

Сегодня говорим про Dependency Injection (DI) и DI фреймворки.

Статью дробим на 3 части:
1. Рассмотрим самый популярный фреймворк Zenject, обозначим недостатки.
2. Выделим достоинства Zenject и дадим общие рекомендации по выбору фреймворка.
3. Проиллюстрирую ряд обозначенных проблем примерами кода.

Итак, поехали 🚀

Самый распространенный и популярный DI фреймворк - Zenject, существует с 2014 года.
Zenject используется как независимыми разработчиками, так и крупными игровыми студиями, среди которых: Square Enix, Electronic Arts, Ubisoft, Zenimax Online Studios и многие другие.

Zenject - будет основной линией данной статьи, на нем будем приводить примеры.

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

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

1. отсутствие полного контроля над жизненным циклом объектов.
Zenject основывается на автоматическом внедрении зависимостей и может привести к утечкам памяти при некорректном уничтожении объектов

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

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

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

5. неявное внедрение зависимости через атрибут [Inject]

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

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

7. усложнение процесса обновления Unity до новых версий из-за потенциальных несовместимостей с новыми API

8. портирование Zenject проекта на другие фреймворки.


Продолжение 👇
#архитектура #dependency_injection #di #zenject


Преимущества Zenject:

1. инверсия управления (IoC) и внедрение зависимостей (DI) помогают ослабить зацепление классов и улучшить тестируемость кода

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

3. интеграция с Unity позволяет использовать DI в контексте игровых объектов и компонентов

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

Zenject оптимален для слаженных и опытных командам с малой текучкой кадров и высоким уровнем компетенции.

Общие советы при выборе DI фреймворка:

- опирайтесь на потребности проекта и компетенцию команды разработчиков

- помните, использование DI фреймворков не всегда обязательно, зависит от целей проекта и его масштаба. Например, разработка прототипа не всегда требует использования DI фреймворка, так как направлена на быстрое создание простой модели продукта для проверки гипотез и его концепции

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

- учитывайте наличие документации и наличие сообщества у выбранного фреймворка

- протестируйте выбранный DI фреймворк перед его использованием в реальном проекте.

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

Жду вопросы в комментариях!

В следующей публикации приведу примеры-иллюстрации проблемных мест Zenject👇
#архитектура #dependency_injection #di #zenject #код


Рассмотрим пример кода проблемных мест на базе Zenject обозначенных в предыдущей публикации.

Пример 1. Проблемы с производительностью

public class SomeClass
{
private readonly SomeDependency _dependency;

public SomeClass(SomeDependency dependency)
{
_dependency = dependency;
}

public void DoSomething()
{
// use _dependency
}
}

public class SomeDependency
{
public SomeDependency()
{
// do something expensive
}
}

public class SomeInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<SomeClass>().AsSingle().NonLazy();
Container.Bind<SomeDependency>().AsSingle().NonLazy();
}
}


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

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


Пример 2. Проблемы с циклической зависимостью

public class SomeClass : MonoBehaviour
{
[Inject] private SomeDependency _dependency;

private void Start()
{
_dependency.DoSomething();
}
}

public class SomeDependency
{
private readonly SomeOtherDependency _otherDependency;

public SomeDependency(SomeOtherDependency otherDependency)
{
_otherDependency = otherDependency;
}

public void DoSomething()
{
// use _otherDependency
}
}

public class SomeOtherDependency
{
public SomeOtherDependency(SomeDependency someDependency)
{
someDependency.DoSomething();
}
}

public class SomeInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<SomeDependency>().AsSingle();
Container.Bind<SomeOtherDependency>().AsSingle();
}
}


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

Пример 3. Неявное внедрение зависимости через атрибут [Inject]

public class MyComponent : MonoBehaviour
{
private IMyDependency _myDependency;

public void DoSomething()
{
_myDependency.DoStuff();
}
}

И интерфейс, который определяет эту зависимость:

public interface IMyDependency
{
void DoStuff();
}

То, чтобы внедрить зависимость в MyComponent, нужно добавить атрибут [Inject] к полю _myDependency:

public class MyComponent : MonoBehaviour
{
[Inject]
private IMyDependency _myDependency;

public void DoSomething()
{
_myDependency.DoStuff();
}
}


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

Проблемы использова
ния атрибута [Inject]:

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

2. Ошибки времени выполнения.

В случае, если некоторые зависимости не были правильно настроены

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

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

Благодарим за внимание!
Хороших выходных 😉
Друзья 👋 Мы подготовили опрос и предлагаем поучаствовать ➡️Есть async метод, в нем выполняется долгая операция, не связанная с объектами Unity. В методе нет асинхронных вызовов и кода для явного запуска новых тредов. Что произойдет при await метода?
Final Results
18%
метод выполнится на главном треде, но чуть позже
43%
синхронно будет выполнен код метода
20%
метод будет обработан отдельным тредом из тред пула
19%
не знаю / требуется звонок другу ☎️
#ассинхронность #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
Схема “Связи между классами”