Система сообщений или “мягкая связь” между компонентами для Unity3D

Введение


В данной статье будут затронуты темы, связанные с реализацией возможности “мягкой связи” компонентов игровой логики на основе системы сообщений при разработке игр на Unity3D.

Ни для кого не секрет, что в подавляющем большинстве случаев средств, которые предоставляет движок в базовом виде, недостаточно для полноценной реализации систем обмена данными между компонентами игры. В самом примитивном варианте, с которого все начинают, мы получаем информацию через экземпляр объекта. Получить этот экземпляр можно разными способами от ссылки на объект сцены, до функций Find. Это не удобно, делает код не гибким и заставляет программиста предусматривать множество нестандартных поведений логики: от “объект исчез из сцены”, до “объект не активен”. Помимо прочего может страдать и скорость работы написанного кода.

Прежде, чем приступить к рассмотрению способов решения проблемы, остановимся подробнее на ее предпосылках и базовых терминах, которые будут упомянуты ниже. Начнем с того, что подразумевается под “мягкой связью”. В данной случае — это обмен данными между компонентами игровой логики таким образом, чтобы эти компоненты абсолютно ничего не знали друг о друге. Исходя из этого, любые ссылки на объекты сцены или же поиск объекта в сцене по имени или типу дают нам “жесткую связь”. Если эти связи начнут выстраиваться в цепочки, то в случае необходимости изменения поведения логики программисту придется все перенастраивать заново. Как не сложно догадаться, гибкостью здесь и не пахнет. Конечно, можно написать расширение редактора, которое будет автоматически заполнять ссылки, но это не решит другую проблему – покомпонентное тестирование игровой логики.

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

Остановимся более подробно на базовом принципе реализации “мягкой связи”. Как было сказано выше, чтобы “мягко” связать два компонента, мы должны передать данные от одного к другому, так, чтобы они не знали ничего друг о друге. Для того, чтобы это обеспечить, нам нужно получить данные не по запросу (имея на руках экземпляр объекта), а использовать механизм уведомлений. Фактически это означает, что при наступлении каких-либо событий в объекте/компоненте, мы не спрашиваем этот объект о его состоянии, объект сам уведомляет о том, что в нем произошли изменения. Набор таких уведомлений формирует интерфейс (не путать с interface в C#), с помощью которого игровая логика получает данные о нашем объекте. Наглядно это можно представить следующим образом:



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

Рассмотрим более подробно способы реализации описанного выше механизма, а также их плюсы и минусы.

Система сообщений на основе UnityEvents/UnityAction


Данная система появилось сравнительно недавно (в 5 версии движка Unity3D). Пример того, как реализовать простую систему сообщений можно посмотреть по этой ссылке.

Плюсы использования данного способа:

  • Встроенная в Unity возможность.

Минусы:

  • Встроенная в Unity возможность (не всегда родные системы лучше).
  • Не гибко из-за использования UnityAction (хотя это можно обойти), который дает ограничение на количество параметров (четыре максимум).
  • Не гибко из-за сложностей с изменением параметров сообщения, так как придется во многих местах кода менять типы, обработчики и т.п..
  • Непонятно зачем использовать при наличии в C# event/delegate.

Классическая система C# на Event/Delegate


Самый простой и достаточно эффективный способ реализации связи компонентов на основе уведомлений — это использование пары event/delegate, которая является частью языка C# (подробнее можно почитать в статьях на habr'е или msdn).

Есть много разных вариантов реализации системы сообщений на основе event/delegate, некоторые из них можно найти на просторах интернета. Я приведу пример, на мой взгляд, наиболее удобной системы, однако для начала хочу упомянуть об одной важной детали, связанной с использованием event'ов. Если у события (event) нет подписчиков, то при вызове этого события произойдет ошибка, поэтому перед использованием обязательна проверка на null. Это не совсем удобно. Конечно можно написать обертку для каждого event, где будет проводиться проверка на null, но это еще более не удобно. Перейдем к реализации.

Для начала определяем интерфейс сообщений для нашего объекта логики:
// Message Interface
public partial class GameLogicObject
{    
    public delegate void StartEventHandler();
    public delegate void ChangeHealthEventHandler(int health);
    public delegate void DeathEventHandler();

    public static event StartEventHandler StartEvent;
    public static event ChangeHealthEventHandler ChangeHealthEvent;
    public static event DeathEventHandler DeathEvent;
}


Вызов уведомлений делается следующим образом (пример):
public partial class GameLogicObject : MonoBehaviour
{
    public int Health = 100;

    void Start()
    {
        if (StartEvent != null)
        {
            StartEvent();
        }

        StartCoroutine(ChangeHealth());                
    }    

    IEnumerator ChangeHealth()
    {
        yield return new WaitForSeconds(1f);

        Health = Mathf.Clamp(Health - UnityEngine.Random.Range(1, 20), 0, 100);

        if (ChangeHealthEvent != null)
        {
            ChangeHealthEvent(Health);
        }

        if (Health == 0)
        {
            if (DeathEvent != null)
            {
                DeathEvent();
            }
        }else
        {
            StartCoroutine(ChangeHealth());
        }
    }
}


Интерфейс и логика сформированы, теперь ничто не мешает нам использовать его в любом другом месте и подписаться на нужные уведомления:
public class GUILogic : MonoBehaviour
{
    public Text HealthInfo;
    public Text StateInfo;

    void OnEnable()
    {
        GameLogicObject.StartEvent += GameLogicObject_StartEventHandler;
        GameLogicObject.ChangeHealthEvent += GameLogicObject_ChangeHealthEventHandler;
        GameLogicObject.DeathEvent += GameLogicObject_DeathEventHandler;
    }

    private void GameLogicObject_DeathEventHandler()
    {
        StateInfo.text = "Im died"; 
    }

    private void GameLogicObject_ChangeHealthEventHandler(int healthValue)
    {
        HealthInfo.text = healthValue.ToString();
    }

    private void GameLogicObject_StartEventHandler()
    {
        StateInfo.text = "Im going";
    }

    void OnDisable()
    {
        GameLogicObject.StartEvent -= GameLogicObject_StartEventHandler;
        GameLogicObject.ChangeHealthEvent -= GameLogicObject_ChangeHealthEventHandler;
        GameLogicObject.DeathEvent -= GameLogicObject_DeathEventHandler;
    }
}


Как видно из примера, подписка происходит в методе OnEnable, а отписка в OnDisable и в данном случае она обязательна, иначе гарантирована утечка памяти и исключения по нулевой ссылке (null reference exception), если объект будет удален из игры. Саму подписку можно осуществлять в любой необходимый момент времени, это не обязательно делать только в OnEnable.

Легко заметить, что при таком подходе, мы можем без каких-либо проблем тестировать работу класса GUILogic, даже в отсутствии реальной логики GameLogicObject. Достаточно написать имитатор, использующий интерфейс уведомлений и использовать вызовы вида GameLogicObject.StartEvent ().

Какие плюсы дает нам данная реализация:

  • Стандартный механизм языка C#, как итог 100% кроссплатформенность без танцев с бубнами.
  • Простота реализации системы (без дополнительных вложений в код).

Минусы:

  • Необходимо следить за памятью (отписка от уведомлений). При больших объемах кода, легко забыть отписаться от одного события и потом долго ловить баги.
  • Необходимо отписываться от событий, если объект сцены деактивируется на время, в противном случае для него будет вызван обработчик.
  • Не гибко из-за сложностей с изменением параметров сообщения, так как придется во многих местах кода менять типы, обработчики, вызовы и т.п..
  • Необходимо учитывать, что событие может не иметь подписчиков.

Reflection система сообщений с идентификаций на string


Прежде чем приступить к описанию системы и ее реализации, хотелось бы остановиться на предпосылках, которые толкнули на ее создание. До прихода к этим мыслям в своих приложениях я использовал описанную выше систему на основе event/delegate. Те проекты, которые мне пришлось разрабатывать на тот момент, были относительно простые, в них требовалась скорость реализации, минимум багов на тестах, исключение по максимуму человеческого фактора в фазе разработки игровой логики. Исходя из этого, родился ряд некоторых требований относительно обмена данными между компонентами:

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

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

Идентифицируем метода класса как обработчик события:
[GlobalMessanger.MessageHandler]
void OnCustomEvent(int param)
{         
}


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

  1. Указывать типа события в параметрах атрибута:
    [GlobalMessanger.MessageHandler("CustomEvent")]
    
  2. Использовать тип события в имени метода с префиксом “On” (или любым другим). Я использую именно этот способ, поскольку в 100% случаях, чтобы не путаться именую методы именно так.

Для того, чтобы осуществить подписку в автоматическом режиме, опять же, существует два способа:

  1. Использовать скрипт, которые будет осуществлять поиск всех компонентов на объекте, а затем через рефлексию искать в них методы с атрибутом. Для того, чтобы не добавлять этот скрипт руками, достаточно будет во всех компонентах, где это потребуется, проставить
    [RequireComponent(typeof(AutoSubsciber))]
    

    Я лично считаю этот способ менее удобным, чем второй, поскольку требует лишних телодвижений.
  2. Создание обертки над классом MonoBehaviour:

    CustomBehaviour
    public class CustomBehaviour : MonoBehaviour
    {
        private BindingFlags m_bingingFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy;
    
        protected void Subscribe(string methodName)
        {
            var method = this.GetType().GetMethod(methodName, m_bingingFlags);
    
            GlobalMessanger.Instance.RegisterMessageHandler(this, method);        
        }
    
        protected void Unsubscribe(string methodName)
        {
            var method = this.GetType().GetMethod(methodName, m_bingingFlags);
    
            GlobalMessanger.Instance.UnregisterMessageHandler(this, method);			
        }
    
        protected virtual void Awake()
        {		
            var methods = this.GetType().GetMethods(m_bingingFlags);
            
            foreach(MethodInfo mi in methods)
            {            
                if(mi.GetCustomAttributes(typeof(GlobalMessanger.MessageHandler),  true).Length != 0)
                {                       
                   GlobalMessanger.Instance.RegisterMessageHandler(this, mi);                
                }
            }
        }   
    }
    


Как видно, данная обертка содержит в себе два метода, которые позволяют подписываться и отписываться от события (тип события забирается из имени метода). Они необходимы в случае, если нам нужно осуществить подписку на событие по ходу работы логики. Автоматическая подписка осуществляется в методе Awake. Отписка от событий осуществляется автоматически, но об этом чуть позже.

Определяем систему менеджмента подписок и вызовов событий:
public class GlobalMessanger : MonoBehaviour
{
    private static GlobalMessanger m_instance;
    public static GlobalMessanger Instance
    {
        get
        {
            if(m_instance == null)
            {
                var go = new GameObject("!GlobalMessanger", typeof(GlobalMessanger));

                m_instance = go.GetComponent<GlobalMessanger>();
            }

            return m_instance;
        }
    }

    public class MessageHandler : Attribute { }

    private class MessageHandlerData
    {
        public object Container;
        public MethodInfo Method;        
    }

    private Hashtable m_handlerHash = new Hashtable();
}


Класс GlobalMessanger является обычным компонентом Unity, доступ к которому осуществляется на основе Unity-синглетона. При этом, для этого компонента создается отдельный объект сцены, который существует только внутри нее и будет удален, когда сцена будет выгружена. Поскольку у нас события идентифицируются на основе строк, то информацию о событиях и подписчиках я решил хранить в хеш-таблице.

Теперь нам нужны методы регистрации/удаления подписчиков:
public void RegisterMessageHandler(object container, MethodInfo methodInfo)
{
    var methodName = methodInfo.Name;
    var messageID = methodName.Substring(2);

    if (!m_handlerHash.ContainsKey(messageID))
    {
        RegisterMessageDefinition(messageID);
    }

    var messageHanlders = (List<MessageHandlerData>)m_handlerHash[messageID];

    messageHanlders.Add(new MessageHandlerData() { Container = container, Method = methodInfo }); 			
}    

public void UnregisterMessageHandler(object container, MethodInfo methodInfo)
{
    var methodName = methodInfo.Name;
    var messageID = methodName.Substring(2);

    if (m_handlerHash.ContainsKey(messageID))
    { 
          var messageHanlders = (List<MessageHandlerData>)m_handlerHash[messageID];			

          for (var i = 0; i < messageHanlders.Count; i++)
          {
               var mhd = messageHanlders[i];

               if (mhd.Container == container && mhd.Method == methodInfo)
               {
                    messageHanlders.Remove(mhd);
                    return;
               }
          }							
     }
}


Далее нам нужен метод вызова событий и подписчиков на них:
public void Call(string messageID, object[] parameter = null)
{
    if (m_handlerHash.ContainsKey(messageID))
    {                        
        var hanlderList = (List<MessageHandlerData>) m_handlerHash[messageID];

        for(var i = 0; i < hanlderList.Count; i++)
        {                
            var mhd = hanlderList[i];               
            var unityObject = (MonoBehaviour)mhd.Container;

            if (unityObject != null)
            {
                if (unityObject.gameObject.activeSelf)
                {
                    mhd.Method.Invoke(mhd.Container, parameter);
                }
            }
            else
            {
                m_removedList.Add(mhd);
            }
        }

        for (var i = 0; i < m_removedList.Count; i++)
        {
            hanlderList.Remove(m_removedList[i]);
        }

        m_removedList.Clear();
    }       
}


Как видно, при вызове события происходит проверка на существование объекта и на активность объекта. В первом случае, удаленный объект убирается из подписчиков, во втором же игнорируется при вызове методов обработки события. Таким образом, следить за отпиской событий у удаленного объекта не требуется, все осуществляется автоматически. При этом, если объект был временно деактивирован, то не осуществляется отписка от событий и повторная подписка, а также при вызове наличие подписчиков на событие не обязательно.

Последнее, что требуется от нас, это провести очистку по выгрузке сцены:
void OnDestroy()
{
    foreach(var handlers in m_handlerHash.Values)
    {
        var messageHanlders = (List<MessageHandlerData>)handlers;

        messageHanlders.Clear();
    }

    m_handlerHash.Clear();                        
    m_handlerHash = null;
}


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

Пример использования данной системы:
// Message Interface
public partial class GameLogicObject
{
    public static void StartEvent()
    {
        GlobalMessanger.Instance.Call("StartEvent");
    }

    public static void ChangeHealthEvent(int value)
    {
        GlobalMessanger.Instance.Call("ChangeHealthEvent", new object[] { value });
    }
    
    public static void DeathEvent()
    {
        GlobalMessanger.Instance.Call("DeathEvent");
    }
}
// Event source
public partial class GameLogicObject : MonoBehaviour
{
    public int Health = 100;

    void Start()
    {        
        StartEvent();       

        StartCoroutine(ChangeHealth());                
    }    

    IEnumerator ChangeHealth()
    {
        yield return new WaitForSeconds(1f);

        Health = Mathf.Clamp(Health - UnityEngine.Random.Range(1, 20), 0, 100);

 ChangeHealthEvent(Health);        

        if (Health == 0)
        {
     DeathEvent();            
        }else
        {
            StartCoroutine(ChangeHealth());
        }
    }
}
// Event subsciber
public class GUILogic : MonoBehaviour
{
    public Text HealthInfo;
    public Text StateInfo;

    [GlobalMessanger.MessageHandler]
    private void OnDeathEvent()
    {
        StateInfo.text = "Im died"; 
    }

    [GlobalMessanger.MessageHandler]
    private void OnChangeHealthEvent(int healthValue)
    {
        HealthInfo.text = healthValue.ToString();
    }

    [GlobalMessanger.MessageHandler]
    private void OnStartEvent()
    {
        StateInfo.text = "Im going";
    }
}


Думаю, сразу заметно насколько сократился код по сравнению с event/delegate, что меня лично радует.

Какие плюсы дает нам данная реализация:

  • Автоматическая подписка на события.
  • Нет необходимости следить за отпиской от событий (даже в случае ручной подписки).
  • Относительно простая реализация.
  • Удобное чтение кода, по атрибутам легко видно, кто является обработчиком событий и каких именно.
  • Меньший объем кода по сравнению с event/delegate
  • Нет необходимости думать о том, что у события нет подписчиков.

Минусы:

  • Поскольку отписка от событий происходит в отложенном режиме, на очень больших проектах, возможно это будет забирать лишнюю память, но это легко решаемо за счет ручной отписки событий, аналогично тому как это было показано в разделе про event/delegate.
  • Привязка к строкам, так как если захочется сделать имя события более красивым, придется менять его во многих местах.
  • Нет гибкости по параметрам и типам данных событий, изменения требует многих действий по коду.
  • Возможны проблемы с кроссплатформенностью из-за использования рефлексии.

Reflection система сообщений с идентификаций на типах данных


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

Итак, нам нужно сохранить все плюсы предыдущей системы, но сделать ее гибче и более устойчивой к возможным изменениям в игровой логике. Поскольку основная проблема — это изменения имени события и передаваемых параметров, то возникла идея идентифицировать события именно по ним. Фактически это означает, что любое событие, которое возникает в компоненте характеризуется ничем иным, как данными, которые оно передает. Поскольку мы не можем просто привязаться к стандартным типам (int, float и т.п.), потому что в логике такие данные могут передавать многие компоненты, то логичным шагом было сделать обертку над ними, которая была бы удобной, легко читаемой и однозначно интерпретирующей событие.

Таким образом, наш интерфейс сообщений принял следующий вид:
public partial class GameObjectLogic
{
    [GEvents.EventDescription(HandlerRequirement = GEvents.HandlerRequirementType.NotRequired)]
    public sealed class StartEvent : GEvents.BaseEvent<StartEvent> { }

    [GEvents.EventDescription(HandlerRequirement = GEvents.HandlerRequirementType.Required)]
    public sealed class ChangeHealthEvent : GEvents.BaseEvent<ChangeHealthEvent>
    {
        public int Value { get; private set; }

        public ChangeHealthEvent(int value)
        {
            Value = value;
        }
    }

    [GEvents.EventDescription(HandlerRequirement = GEvents.HandlerRequirementType.Required)]
    public sealed class DeathEvent : GEvents.BaseEvent<DeathEvent> { }
}


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

Метод вызова событий Call (и его перегрузки), который ранее у нас был частью класса GlobalMessanger, теперь является статическим и находится в GEvents.BaseEvent и принимает теперь в качестве параметра экземпляр класса, описывающего тип события.

Код вызова уведомлений теперь будет таким:
public partial class GameLogicObject : MonoBehaviour
{
    public int Health = 100;

    void Start()
    {        
        StartEvent.Call(); //вызов уведомления о событии      

        StartCoroutine(ChangeHealth());                
    }    

    IEnumerator ChangeHealth()
    {
        yield return new WaitForSeconds(1f);

        Health = Mathf.Clamp(Health - UnityEngine.Random.Range(1, 20), 0, 100);

        ChangeHealthEvent.Call(new ChangeHealthEvent(Health)); //вызов уведомления о событии       

        if (Health == 0)
        {
             DeathEvent.Call();  //вызов уведомления о событии         
        }else
        {
            StartCoroutine(ChangeHealth());
        }
    }
}


Подписка и отписка на события, осуществляется тем же самым способом, что и ранее, через атрибуты методов, однако теперь идентификация типа события происходит не по строковому значению (имя метода или параметр атрибута), а по типу параметра метода (в примере это классы StartEvent, ChangeHealthEvent и DeathEvent).

Пример метода обработчика:
[GEvents.EventHandler]
public void OnDeathEventHandler(GameLogicObject.DeathEvent ev)
{ 
    //выполняем действия связанные с событием 
}


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

Итог


Я постарался описать в этой статье все возможные на мой субъективный взгляд способы построения систем обмена данными между компонентами на основе уведомлений. Все эти способы были использованы мной в разных проектах и разной сложности: от простых мобильных проектов, до сложных PC. Какую систему использовать в вашем проекте – решать только вам.

PS: я намеренно не стал описывать в статье построение системы сообщения на основе SendMessage-функций, поскольку по сравнению с остальными она не выдерживает критики не только по удобству, но и по скорости работы.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 26

    0
    не пробовали https://www.assetstore.unity3d.com/en/#!/content/17276?
      0
      Пробовали, это более высокий уровень нежели то, что описано в статье и помимо прочего UniRx он про асинхронное программирование, а там надо несколько все по другому думать в плане архитектуры и т.п.
      +2
      Я стараюсь в своих проектах уйти от концепции событий в сторону обработки состояний объектов. Для понимания, можно посмотреть решение от Wooga – Entitas.

      При таком подходе вся логика хранится в системах, что срабатывают для N-объектов при Х-состоянии объектов. Например, при возникновении объектов с компонентом DamageComponent и HealthComponent, система DamageDealSystem отнимет у HealthComponent необходимое количество жизней и отправит запрос на уничтожение состояния DamageComponent. В свою очередь DamagePopupUISystem система выведет в интерфейс значение из модели Damage (до его уничтожения). Думаю, что не нужно объяснять, что количество систем может быть неограниченно, например, DamageToScoreSystem, DamageReflectSystem, DamageNetworkSyncSystem и т.д.
        +1
        Но самое шикарное, это возможность быстро и безболезненно создавать сохранения, записи игр (реплеи). Так как вся игра на любой момент времени — это набор объектов с их состояниями (плоскими данными без логики)
          0
          Да, есть системы разные, для одних проектов подходят одни для других другие. Но вот в данном случае, DamageDealSystem как-то должно узнать что появился объект с компонентом DamageComponent и HealthComponent.
            0
            В Entitas эти события «появлений» сделаны очень хитро, с помощью так называемых Group Observer. Поскольку каждое изменение значений жизней (например) делается через ReplaceHealth (грубо), то есть как бы меняя один компонент на другой, эти Group Observer имеют возможность сразу понять, что в этот момент нужно что-то делать. Там (в Entitas) на основе этого принципа работают Reactive Systems. Вне систем эти группы можно использовать в любом коде, то есть просто описываем интересующие нас сущности (сущность должна иметь такие-то и такие-то компоненты), подписываемся на событие обновления (грубо говоря) и обрабатываем полученные данные. Таким образом, например, можно сделать чистую вьюшку в интерфейсе, которая мониторит этот Health и просто выводит его на экран.
            Вообще Entitas — это крайне интересная вещь, сейчас как раз делаю мелкий тестовый проект чтобы разобраться. До конца в самой нутрянке работы ещё не копался, но внешний «API» вдохновляет, а общая архитектура приложения получается крайне гибкой и расширяемой, очень советую посмотреть
              0
              В целом понятно, это из разряда UniRx, без «ста грамм» не разобраться и придумать адекватное применение еще сложнее, надо разбираться и долго. Спасибо за наводку, посмотрим этот Entitas.
        0
        Почему бы не использовать DI контейнер для слабосвязанного кода? Например вот очень легковесный пример: github.com/intentor/adic
          0
          DI и IoC это паттерны, которые я считаю применять можно, но не шибко удобно в объектно-компонентном подходе, который я стараюсь использовать при разработке игр на Unity3D.
            0
            В чем заключаются неудобства?
              0
              Ну во-первых, у меня объекты игровой логики не инициализируются в конструкторах, практически никогда. Во вторых, иньекции в конструктор или в свойство так или иначе требуют ссылки на интерфейс, что в моем понятии может и менее жесткая связь, чем прямая ссылка на экземпляр класса, но и не мягкая связь тоже. Я предпочитаю, чтобы компоненты вообще ничего не знали, ни через интерфейсы ни каким либо другим способом друг о друге, т.е. абсолютная независимость. Может конечно я заблуждаюсь, но мой линчый опыт говорит об обратном. К сожалению, я больших проектов мобильных аля Clash of Clans не делал, поэтому не могу сказать как описанные в статье системы поведут себя там, равно как я не знаю применяется ли там DI, в том же смысле, что и система уведомлений.

              В целом, если бы я например писал не игровую логику (реализация игрового процесса), а некую большую обособленную систему, которая использовалась бы в ней, и в этой системе нужно было бы использовать внешние какие-то вещи, тогда да, DI и сам принцип IoC имел бы смысл. В некотором смысле в тех проектах, в которых я участвовал, эти принципы применялись при подключении тех же платежных API для мобильный проектов.
          0
          К недостаткам описанного способа можно так же отнести и сложность отладки. Как правило во время дебага обработчика события стэк вызовов содержит слишком большое количество элементов. А если любите корутины то понять кто же выбросил эвент часто бывает невозможным.
            0
            Тут больше тогда проблема к Coroutine, которые в силу своей асинхронности всегда неудобно отлаживать. В случае синхронного вызова — не такая большая проблема со стеком, если учесть гибкость которую дают подобные системы.
              0
              Согласен, тем более, что описанные системы в своей сути синхронны до мозга костей, это надо было уточнить наверное. Если нужны ассинхронные сообщения, тогда надо использовать несколько другой подход.
            0
            Я лично считаю этот способ менее удобным, чем второй, поскольку требует лишних телодвижений.
            Создание обертки над классом MonoBehaviour:


            Он (метод), конечно удобнее, пока не появится необходимость в наследовании от другого класса. Впрочем за удобства почти всегда приходится расплачиваться гибкостью.
              0
              Другой класс может наследоваться от CustomBehaviour. Поскольку в игровой логике, компоненты так или иначе MonoBehaviour, ничто не мешает сделать
              public class ChildClass: MyClass {}
              public class MyClass: CustomBehaviour {}
              0
              Чем в проекте меньше подписок тем лучше(с). На самом деле, подобное решение крайне сложно поддается дебагу, в большой команде это вообще похоже на ад и целое кладбище костылей. Приведу пример. Встречал программиста, который злоупотреблял методами SendMessage(). Логика такова — «кому нужно тот получит». И вот он ушел с той команды. И… Как дебаг ерланга (что за мессадж, от кого пришел, кому должен, зачем должен и тд.) В итоге систему частично выпилили. Я бы порекомендовал использовать евентовую систему в виде классов. MVC одним словом. Тогда даже если есть подписка, всегда можно поймать откуда ушел класс, куда и зачем (например юзать поле Sender или что-то типа него).
                0
                На самом деле, при синхронном вызове, узнать откуда пришло событие достаточно просто. Поскольку описанные выше системы построены на вызове функций сохраненных в хеше через рефлексию, то фактически происходит синхронный вызов методов у других классов через прослойку скрывающую от нас реализацию этих классов. Точно также можно ставить точки остановки, получать стэк вызовов (а если пошаманить в Unity, можно получить полный стэк).

                Самая большая проблема это ассинхронные сообщения, но это совершенно отдельная история.

                SendMessage это вообще рудимент, использовать его для системы сообщений мягко сказать плохо. Единственный его плюс, в возможности вызвать метод у компонента, без получения экземпляра класса, причем любой.
                +2
                А что насчет сигналов и комманд? Я вот последнее время использую фреймворк StrangeIoC для DI и плюс сигналы-комманды. Можно создавать цепочки команд, можно подписаться в любом месте на сигнал.

                  0
                  Интересно, но есть вопросы. А как это система относится к изменениям в цепочках команд? И что если в цепочке у нас может быть 8-10 событий? Или например в каких-то командах после тестов гейм-дизайнерами необходимо поменять передаваемые данные? Или нам надо протестировать в этой цепочки источник и конечного элемент, а у нас еще не готов код промежуточных команд?
                    0
                    А как это система относится к изменениям в цепочках команд?
                    В разделе Mapping command написано, что процесс биндинга команд к сигналам можно выполнять где и когда угодно. Если вы об этом.
                    И что если в цепочке у нас может быть 8-10 событий?
                    Событий или команд? Если команд, то системе все равно, какое кол-во. Так же, как и синхронная команда или асинхронная. Для этого нужно лишь правильно реализовать команду.
                    Или например в каких-то командах после тестов гейм-дизайнерами необходимо поменять передаваемые данные?
                    Первый способ — вызывать сигнал с параметрами, тогда эти параметры будут передаваться в команду. Второй — ничего не мешает в самой команде обратиться к сервису или модели. Например:
                    class StartGameCommand : EventCommand
                    	{
                    		[Inject]
                    		public ITimer gameTimer{get;set;}
                    
                    		override public void Execute()
                    		{
                    			gameTimer.start();
                    		}
                    	}
                    

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

                    Если данная тема интересна кому-нибудь еще кроме Вас, то мог бы написать статью. Но на первых порах достаточно прочитать документацию.
                      0
                      Мне интересна, буду рад если напишите.
                  0
                  При включении поддержки .NET 4.5 и использовании C# 6.0 (что уже повсеместно в проектах), можно немного видоизменить код.
                  До:
                  if (StartEvent != null)
                  {
                      StartEvent();
                  }

                  После:
                  StartEvent?.Invoke();
                    0
                    Да, это пришло с 2018 версию (2017 это еще было экспериментальным), а статья писалась когда еще Unity 5 был актуален
                    0
                    Само использование рефлексии крайне ресурсоёмко. Выполнение одноразово при инициализации экземпляра немногочисленного набора объектов — допустимо, но рефлексия для каждого подписчика при наступлении каждого события в игре — ужасная практика. Вы сами описали эти события: выстрел, попадание пули, отнимание здоровья и т.д. И это только примитивный набор функционала для тестовой игры. Даже в малом проекте в продакшене сотни связей и вызовов методов других классов, если всё это реализовывать на рефлексии — всё процессорное время будет тратиться на её обработку. (Извиняюсь за тавтологию) Рефелексия — bad practice и используется только в самых крайних случаях, а не тогда, когда Вам лень проверять Event на null. Многое из проделанного — излишне. В интернете множество графиков сравнений скорости работы различных методов вызова функций. Например:
                    habr.com/post/353780
                    Здесь ясно описана разница различных методов. Ваш подход уступает в производительности в 55-90 раз. Вы считаете это хорошим результатом?
                      0
                      В этой статье описаны методы, но не конечные реализации, прочитайте про способы ускорения вызова методов, полученных через рефлексию в обход MethodInvoke (который и является самым тормозным способом). Рефлексия bad practiсe, для тех кто не понимает когда, как и зачем.
                      А в приведенной статье, про сравнение сокростей, как раз таки рефлексия даже не используется.

                    Only users with full accounts can post comments. Log in, please.