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

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

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

Суть игры:

Игрок бросает от 1 до "условно бесконечного числа" игральных костей, получая очки от выпавших значений. Дефолтно 1 кубик.
За накопленные очки можно апгрейдить грани кубика (каждую отдельно) либо покупать дополнительные кубики.
При апгрейде выбирается грань кубика и увеличивается количество точек на ней (до 10 - выражены точками, дальше - числами).

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

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


Поехали.
Считаю, что любой куб можно апгрейдить.
Разделим проект на слои: Content, State, Entity, Pm, View.

Иерархия контента:
- Content, содержит поле id.
- Cube, наследник Content. Поля: side1, side2, side3, side4, side5, side6.
- CubeLevel1, наследник Cube. Cодержит начальные характеристики куба уровня 1.
- ...
- CubeLevel[n], наследник Cube. Cодержит начальные характеристики куба уровня [n].
- CubeUpgrade, наследник Content.
- CubeUpgradeLevel2, наследник CubeUpgrade. Поля: from (куб, с которого апгрейд им), to (куб, на который апгрейдим).
- CubeUpgradeLevel[n], -//-.

- CubeSide, наследник Content
- CubeSideLevel1, наследник CubeSide, условно - грань уровня 1, содержит начальные характеристики грани уровня 1.
- ...
- CubeSideLevel[n], ..., наследник CubeSide, грань уровня [n], содержит начальные характеристики грани уровня [n].
- CubeSideUpgrade, наследник Content.
- CubeSideUpgradeLevel2, наследник CubeSideUpgrade. Поля: from (грань, с какой апгрейдим), to (грань, на которую апгрейдим).
- ...
- CubeSideUpgradeLevel[n], -//-.

Иерархия стейтов:
- State. Поля: id, contentId.
- CubeState, наследник State. Содержит изменяемые характеристики куба: уровень апгрейда, rotate, position, набор стейтов граней, кулдаун на следующий апгрейд, наложенные бафы, и т.д.
- CubeSideState, наследник State. Содержит изменяемые характеристики грани: индекс грани, уровень апгрейда, кол-во точек, кулдаун на следующий апгрейд, наложенные бафы, и т.д.

PlayerState - игровой профиль. Считаю, что уже существует. Его поля содержат:
- ...
- cubeStates, коллекция со стейтами кубов.

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

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

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

Приведу пример:
ReactiveCommand<string, string> testStringResultCommand = new ReactiveCommand<string, string>();

testStringResultCommand.Subscribe(inputValue =>
{
return $"{inputValue}def";
});

string resultNormal = testStringResultCommand.Execute("abc");

Debug.Log(resultNormal); <- abcdef

testStringResultCommand.Dispose();

В UniRx, к сожалению, такое поведение отсутствует.

Честно скажу, что в своих проектах, я обходил стороной такой подход, и до сих пор считаю, что если такое назревает, то с архитектурой проекта МОЖЕТ быть что-то не так.

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

Тк запрос был частым, я решил немного доработать код ReactiveCommand из UniRx и выложить сюда.
Чтобы не ломать codestyle, который используется в UniRx, писал в их стиле.

Как пользоваться и осноные нюансы:

ReactiveCommand<string, string> testStringResultCommand = new ReactiveCommand<string, string>();

testStringResultCommand.Subscribe(inputValue =>
{
return $"{inputValue}def";
});

string resultNormal = testStringResultCommand.Execute("abc");

Debug.Log(resultNormal); <- abcdef

testStringResultCommand.Dispose();

string resultDisposed = testStringResultCommand.Execute("abc"); <- ObjectDisposedException: Cannot access a disposed object.

Если вызвать Dispose для команды, то при попытке сделать Execute будет выбрасываться исключение: ObjectDisposedException: Cannot access a disposed object.
Если вызвать Dispose для команды, то при попытке сделать Subscribe будет выбрасываться исключение: ObjectDisposedException: Cannot access a disposed object.
Если вызвать Dispose для подписки, то обработчик больше не будет срабатывать
Если несколько раз подписаться на команду, то будет срабатывать последняя подписка, поэтому лучше избегать ситуацию с несколькими подписками.

Файл можно скачать ниже👇
Всем хороших выходных!
Приветствую 👋

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

Далее при наличии достаточного количества желающих можем запланировать и провести видеострим.

Для понимания запроса просьба поставить реакцию всем, кто хотел бы присутствовать.

Хорошего вечера.
Приветствую, друзья 👋

Код для игрового кубика написан 💪
Разбор в процессе!

Позже будет полезный пост по Rider, как настроить автоматическое удаление ненужных "using".
Media is too big
VIEW IN TELEGRAM
#лайфхак #rider

Приветствую
👋
Прикрепляю видео по Rider с инструкцией по настройке автоматического удаления ненужных "using".

Практическая ценность - чистота кода.

Хорошего дня
Приветствую, друзья👋 Как вы? Удалось посмотреть разбор и подготовить список вопросов?
Отдайте свой голос, какой из вариантов по стриму для вас более привлекателен 👇
Anonymous Poll
19%
Будни после 19:00 Мск
22%
Выходной
22%
НГ каникулы 🎄🥂🍾
36%
Любой из вариантов
Друзья, с наступающим Новым Годом 🥂🍾
Пусть он принесет вам рост и новые возможности 🙌
С 2024 годом 🎄
Друзья, приветствую 👋

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

Из приятного: на текущий момент готовлю для вас статью, посвященную различию между реактивными объектами и делегатами ✍️.

Обещанный стрим постараюсь поставить на 17 - 18 февраля.

Хорошего дня!
Друзья, приветствую 👋 Я часто слышал аргумент: "Зачем реактивщина, ведь тоже самое можно сделать на делегатах". Пока я готовлю статью на эту тему, предлагаю рассмотреть пример (код в комментарии) и выбрать ответ.
Final Results
71%
Все ок, сработают две подписки, т.к. делегат - это ref тип
21%
Не ок, делегат - это value тип, вторая подписка не сработает
8%
Свой вариант ответа
Приветствую 👋
Стрим планируем на это воскресенье (18.02) 16:00 Мск
Буду раз всех вас видеть
Всем привет, напоминаю, что через 45 мин начинаем стрим, стрим будет проходить в этом канале.
Live stream finished (420 days)
#архитектура #делегаты #event #action #Unity3D

Реактивное программирование vs делегаты в C#, Unity 3D


Приветствую 👋
Сегодня говорим про реактивное программирование vs делегаты (delegate) в C#, Unity 3D.
Для начала поясню, почему в статье я рассматриваю именно delegate, а не event или Action.

В C# Action, event и delegate являются ключевыми элементами, используемыми для работы с методами как с объектами. Это позволяет реализовывать такие концепции, как обратные вызовы (callbacks) и событийно-ориентированное программирование.

Delegate является основой для Action и event, где
- Action предоставляет удобный способ использования делегатов без возвращаемого значения
- event использует делегаты, обеспечивая контролируемый способ подписки на уведомления и обработки событий.
Поэтому в статье я буду рассматривать именно delegate.

Но для начала приведу основные отличия delegate, Action, event:
- Delegate - это тип, который представляет ссылки на методы с определённой сигнатурой и возвращаемым типом.
Это означает, что мы можем использовать переменные delegate для хранения ссылок на методы.
Delegate обеспечивает возможность передачи методов как аргументов методам или в качестве типов возвращаемых значений.
Delegate поддерживает многоадресные вызовы, позволяя вызывать несколько методов подписанных на делегат (multicast делегаты).
- Action - это обобщённый делегат, который не возвращает значение (возвращаемый тип void) и может принимать от 0 до 16 параметров.
Action является удобным способом использования делегатов, не требуя объявлять новый тип делегата для каждой сигнатуры метода.
По сути, Action предназначен для ситуаций, когда нужно передать методы, не возвращающие значение.
- Event - это способ, с помощью которого класс или объект может предоставлять уведомления.
Итак event (события):
-используют делегаты для обработки уведомлений.
- реализует паттерн Observer, где издатель уведомляет подписчиков о том, что произошло определённое событие.
- ограничивают способность внешнего кода вызывать делегат. Внешний код может подписаться на событие или отписаться от него, но не может напрямую вызвать делегат события.
- добавляют уровень инкапсуляции к делегатам, обеспечивая более безопасную и управляемую модель подписки на уведомления.

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


продолжение 🔽
#архитектура #делегаты #event #action #Unity3D

Реактивное программирование vs делегаты в C#, Unity 3D продолжение 🔽

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

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

В реактивном программировании необходимо помнить, как работает ReactiveProperty и ReactiveCommand. Краткий обзор.

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

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

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

Продолжение 🔽
#архитектура #делегаты #event #action #Unity3D

Реактивное программирование vs делегаты в C#, Unity 3D продолжение
🔽

Делегаты в C# - это типы, которые представляют собой ссылки на методы.

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

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

Минусы:
- Сложности с multicast делегатами.
Хотя multicast делегаты предоставляют мощные возможности, управление порядком вызова и обработка возвращаемых значений могут быть сложными.
- Ограниченность в сравнении с реактивным программированием.
Делегаты могут быть менее выразительными при работе с сложными потоками данных по сравнению с реактивным программированием.
- Неявные тонкости, которые могут запутать начинающих разработчиков.

Продолжение 🔽
Теперь вернёмся к опросу
К сожалению, корректный корректно ответила меньшая часть опрашиваемых
.

public class Root : BaseMonoBehaviour 
{
private delegate void DealDamage();


private void Start()
{
// инстанцируем делегат с лямбдой - заглушкой
DealDamage dealDamage = () =>
{
Debug.Log("Deal damage stub");
};

// тут условно передаем делегат в слой, который отвечает за вызов делегата
PassDealDamageToAnotherLayer(dealDamage);

// подписываемся логикой на делегат
dealDamage += () =>
{
Debug.Log("Deal damage subscriber");
};
}

private void PassDealDamageToAnotherLayer(DealDamage dealDamage)
{
// через 5 секунд сделаем вызов делегата
Observable.Timer(TimeSpan.FromSeconds(5)).Subscribe(_ =>
{
dealDamage();
});
}
}


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

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

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

Приведу пример альтернативной вариация корректно работающего кода, в котором ссылка на делегат обновляется постоянно:

public class Root : BaseMonoBehaviour 
{
private delegate void DealDamage();

private DealDamage _dealDamage;

private void Start()
{
// инстанцируем делегат с лямбдой - заглушкой
_dealDamage = () =>
{
Debug.Log("Deal damage stub");
};

// тут условно передаем делегат в слой, который отвечает за вызов делегата
PassDealDamageToAnotherLayer();

// подписываемся логикой на делегат
_dealDamage += () =>
{
Debug.Log("Deal damage subscriber");
};
}

private void PassDealDamageToAnotherLayer()
{
// через 5 секунд сделаем вызов делегата
Observable.Timer(TimeSpan.FromSeconds(5)).Subscribe(_ =>
{
_dealDamage();
});
}
}

Подытожу Выбор между реактивным программированием и делегатами в C# зависит от специфики задачи и глубины понимания каждого из подходов.

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

Всем хороших выходных 🖐
Дорогая женская часть нашего канала ❤️

Поздравляем вас с 8 Марта, с Международным Женским Днем 🥳🥂🍾

Желаем реализации в каждой из выбранных сфер и интересных задач!
Больше улыбок и положительных эмоций!

С праздником 💐
Всем привет 👋 Предлагаю принять участие в опросе! На собесах часто спрашивают ➡️ Сколько поколений (количество) использует GC в Unity? Вводные: используем il2cpp backend с выключенным Incremental GC.
Final Results
35%
не использует поколения
7%
1
26%
2
28%
3
4%
Свой вариант ответа