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

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

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

Напомню вводные В игре есть турель (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%
Нет
#вопрос_ответ

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

Суть игры:

Игрок бросает от 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. Краткий обзор.

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

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

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

Продолжение 🔽