Как стать автором
Обновить

Комментарии 18

ешё бы демку на Гите, чтобы можно было поковырять - было бы супер =)

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

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

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

По этой статье совершенно непонятно, зачем нужна обертка вокруг делегата, которая делает в точности то же самое, что и он? Также странное решение делать публичные переменные, обернутые в обертки. Какой-то ужасно приторный синтаксический сахар просто чтобы показать, что автор умеет писать шаблонные методы. Все это можно накодить в 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, который трудно поддерживать в долгую, либо бесконечное множество способов сделать что-то ещё в ООП стиле. И это пока один из вариантов лучших, что я видел. Выглядит достаточно просто, логика раздроблена, каши из множества монобехов нет, и параметры в инспекторе в рантайме покрутить можно, потому что ссылочная обёртка.
Прям есть что перенять и запомнить для своих проектов)
Больше спасибо за статью.

Благодарю за обратную связь, сам только на 5-й год допер :)

Зарегистрируйтесь на Хабре, чтобы оставить комментарий