Comments 18
ешё бы демку на Гите, чтобы можно было поковырять - было бы супер =)
Вот ссылка на архив демки: https://drive.google.com/file/d/1f9dK9s0H7jq2EDohmd8MEZ4xxuJQIvQJ/view?usp=sharing
Так нравятся эти статьи. Их поймет только человек уже разбирающийся в этом, но оно ему как бы и не надо. А новичок будет смотреть как на китайский и ничего не поймет. Для кого тогда статьи пишутся?
Если на самом деле хочется гайд сделать - мало выложить код и сказать "работает, смотрите". В гайде бы пояснять код поэтапно, приводить в пример альтернативы, обосновывать эффективность той или иной строчки.
По этой статье совершенно непонятно, зачем нужна обертка вокруг делегата, которая делает в точности то же самое, что и он? Также странное решение делать публичные переменные, обернутые в обертки. Какой-то ужасно приторный синтаксический сахар просто чтобы показать, что автор умеет писать шаблонные методы. Все это можно накодить в 3 строчки кода, если просто правильно пользоваться знанием среды и языка, не изобретая велосипед. Возможно, я неправ.
Подход делить логики и использовать делегаты сам по себе неплох. Но это тоже подходит не для всего. В некоторых случаях просто необходимо забить логику гвоздями просто чтобы никто ее не сломал неправильной настройкой компонентов. А делегаты - это больше чтобы звуки и партиклы на персонажа вешать
Обертка вокруг делегата нужна для того, чтобы у была ссылка на объект делегате, чтобы в будущем на него можно было подписаться с другими объектами. С примитивными типами тоже самое. Если бы в C# были бы полноценные инструменты для работы с указателями как в C++, то обертки не понадобились бы
Приведу пример, как можно сделать делегат с параметрами без создания обертки, так это обычно и используют:
public class Health {
public delegate void DamageTaken(float damage);
public event DamageTaken OnDamageTaken; // will be triggered on damage
public delegate void PlayerDead(); // will be triggered when dead.
public event PlayerDead OnDead;
private float health = 100;
// call this to do damage
public void TakeDamage(float damage) {
if (health <= 0) return;
health -= 20;
if (OnDamageTaken != null) // null check is required because if there is no subscribers it produces error when invoking
{
OnDamageTaken.Invoke(damage);
}
if (health <= 0) {
if (OnDead != null) {
OnDead.Invoke();
}
}
}
}
С переменными идея понятна, т.к. хочется, чтобы можно было подписываться на изменение переменной. Но всё равно делать их публичными - очень плохое архитектурное решение. Если нужно подписаться на изменение здоровья извне, надо создать соответствующий ивент или делегат. Иногда можно нарушать правила, но в подобных статьях я не делал бы этого. Новичков и так постоянно учат плохому.
Насчет ссылок и указателей плохо понял. В С# любая переменная, содержащая объект, это аналог shared_ptr из C++ со всеми вытекающими проблемами. Есть кстати в C# и обычные C указатели, ими можно пользоваться в unsafe коде. Узнал недавно и удивился :)
P.S.: в этом примере делается проверка на null, но есть более короткая форма OnDamageTaken?.Invoke(damage);
Приведенный класс Health написан хорошо, так как соблюдает принцип ед. ответственности и является автономным (Open-Closed Principle). Но поскольку он совмещает данные и логику, мб проблематично добавлять новые механики, которые будут пересекаться с механикой здоровья. Например, если мы захотим сделать броню, которая будет поглощать часть урона, то придется переписывать класс Health, поэтому рекомендую отделить данные от логики, например так, без использования AtomicVariable<T> и AtomicEvent:
//Данные:
public sealed class HealthData {
public Action<int> TakeDamageRequest;
public Action<int> TakeDamageEvent;
public Action DeathEvent; //Тут можно и свои делегаты
public int health;
}
//Логика:
public sealed class TakeDamageMechanics {
private HealthData data;
public TakeDamageMechanics(HealthData data)
{
this.data = data;
}
public void OnEnable()
{
this.data.TakeDamageRequest += OnTakeDamage;
}
public void OnDisable()
{
this.data.TakeDamageRequest -= OnTakeDamage;
}
private void OnTakeDamage(int damage)
{
if (this.data.health <= 0)
{
return;
}
this.data.health -= damage;
this.data.OnDamageTaken?.Invoke(damage);
if (this.data.health <= 0) {
this.data.OnDead?.Invoke();
}
}
}
Да, делегат можно создать без обертки, но вот получить ссылку в чистом виде на событие OnDamageTaken не получится, так как язык C# это не позволяет. Модификатор unsafe действительно позволяет работать с указателями, но обычно он используется в блоке метода. Но передать указатель на поле класса не получится
public unsafe class A {
public int health;
public unsafe void Do()
{
var b = new B(&this.health) //Нельзя!!!
}
}
А ничего что Update вызывается каждый кадр? (Это по поводу "не использовать coroutine"). А теперь представьте, что вы хотите сделать сетевой шутер на кубах, тогда к вашим AtomicVariable придётся прикручивать сериализации и десереиализации, а это будет работать ой как не быстро, и ни один модуль не подцепит их нативно, типа миррора.
И что что апдейт вызывается каждый кадр. Если нужно увеличить производительность, то в некоторых случаях можно работать через подписку, чтобы уменьшить частоту вызовов методов.
По поводу сетевых взаимодействий — да придется прикручивать свой сериализатор. Вопрос производительности зависит от способа сбора данных с игровых объектов.
Тут необязательно делать прям AtomicVariable & AtomicEvent и через них работать. Смысл заключается в принципе разделения данных и логики. Можно сделать любой контейнер данных, который легко и быстро можно сериализовывать для увеличения производительности
В приведенном примере CanMoveMechanics
может работать не через Update()
а через подписку на изменение значения isDead
ведь для этого у AtomicVariable
и есть ивент onValueChanged
Несмотря на опыт в пару лет всё равно часто думаю, а как "удобно" организовывать логику в ООП подходе.
В ECS всё как правило просто и понятно, а в ООП либо Unity-way, который трудно поддерживать в долгую, либо бесконечное множество способов сделать что-то ещё в ООП стиле. И это пока один из вариантов лучших, что я видел. Выглядит достаточно просто, логика раздроблена, каши из множества монобехов нет, и параметры в инспекторе в рантайме покрутить можно, потому что ссылочная обёртка.
Прям есть что перенять и запомнить для своих проектов)
Больше спасибо за статью.
Модульные механики на Unity