Как я могу избежать жесткой связи скриптов в Unity?

24

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

Например:

Я хочу иметь системы здоровья и смерти в отдельных сценариях. Я также хочу иметь различные взаимозаменяемые сценарии ходьбы, которые позволяют мне изменять способ перемещения персонажа игрока (основанные на физике, инерционные элементы управления, как в Mario, по сравнению с жесткими, дергающимися элементами управления, как в Super Meat Boy). Сценарий Health должен содержать ссылку на скрипт Death, чтобы он мог вызывать метод Die (), когда здоровье игроков достигает 0. В скрипте Death должна содержаться некоторая ссылка на используемый скрипт прогулки, чтобы отключить хождение на смерть (I устал от зомби).

Я обычно создавать интерфейсы, как IWalking, IHealthи IDeath, таким образом , что я могу изменить на наитию эти элементы , не нарушая остальную часть моего кода. Скажем, я бы настроил их с помощью отдельного сценария на объекте player PlayerScriptDependancyInjector. Возможно , что сценарий будет иметь общественные IWalking, IHealthи IDeathатрибуты, так что зависимости может быть установлен конструктором уровня от инспектора, перетаскивая соответствующие сценарии.

Это позволило бы мне просто добавить поведение в игровые объекты и не беспокоиться о жестко закодированных зависимостях.

Проблема в единстве

Проблема в том , что в Unity я не могу выставить интерфейсы в инспекторе, и если я пишу свои собственные инспекторы, ссылки не получите сериализовать, и это очень много ненужной работы. Вот почему я остался с написанием тесно связанного кода. Мой Deathсценарий предоставляет ссылку на InertiveWalkingсценарий. Но если я решу, что хочу, чтобы персонаж игрока жестко контролировал, я не могу просто перетащить TightWalkingсценарий, мне нужно изменить Deathсценарий. Это отстой. Я могу с этим справиться, но моя душа плачет каждый раз, когда я делаю что-то подобное.

Какая предпочтительная альтернатива интерфейсам в Unity? Как мне решить эту проблему? Я нашел это , но оно говорит мне то, что я уже знаю, и не говорит мне, как это сделать в Unity! Здесь также обсуждается, что нужно делать, а не как, это не решает проблему тесной связи между сценариями.

В целом, я чувствую, что они написаны для людей, которые пришли в Unity с опытом разработки игр, которые просто учатся программировать, и у Unity очень мало ресурсов для обычных разработчиков. Есть ли какой-нибудь стандартный способ структурировать ваш код в Unity, или мне нужно найти свой собственный метод?

KL
источник
1
The problem is that in Unity I can't expose interfaces in the inspectorЯ не думаю, что понимаю, что вы подразумеваете под «разоблачить интерфейс в инспекторе», потому что моей первой мыслью было «почему бы и нет?»
Джоккинг
@jhocking спроси разработчиков единства. Вы просто не видите этого в инспекторе. Он просто не появляется там, если вы устанавливаете его как открытый атрибут, а все остальные атрибуты появляются.
KL
1
ооо, вы хотите использовать интерфейс в качестве типа переменной, а не просто ссылаться на объект с этим интерфейсом.
Джоккинг
1
Вы читали оригинальную статью ECS? cowboyprogramming.com/2007/01/05/evolve-your-heirachy А как насчет дизайна SOLID? en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29
JZX
1
@jzx Я знаю и использую SOLID design, и я большой поклонник книг Роберта С. Мартинса, но этот вопрос - о том, как это сделать в Unity. И нет, я не читал эту статью, читая ее прямо сейчас, спасибо :)
KL

Ответы:

14

Есть несколько способов избежать жесткой связи скриптов. Внутренним для Unity является функция SendMessage, которая при нацеливании на объект GameObject монобиха отправляет это сообщение всем объектам игры. Таким образом, у вас может быть что-то вроде этого в вашем объекте здоровья:

[SerializeField]
private int _currentHealth;
public int currentHealth
{
    get { return _currentHealth;}
    protected set
    {
        _currentHealth = value;
        gameObject.SendMessage("HealthChanged", value, SendMessageOptions.DontRequireReceiver);
    }
}

[SerializeField]
private int _maxHealth;
public int maxHealth
{
    get { return _maxHealth;}
    protected set
    {
        _maxHealth = value;
        if (currentHealth > _maxHealth)
        {
            currentHealth = _maxHealth;
        }
    }
}

public void ChangeHealth(int deltaChange)
{
    currentHealth += deltaChange;
}

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

void HealthChanged(int newHealth)
{
    if (newHealth <= 0)
    {
        Die();
    }
}

void Die ()
{
    Debug.Log ("I am undone!");
    DestroyObject (gameObject);
}

Однако в этом нет гарантии порядка. По моему опыту, он всегда идет от самого верхнего сценария в GameObject к нижнему большинству сценариев, но я бы не стал рассчитывать на то, что вы сами не сможете наблюдать.

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

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

Бретт Сатору
источник
5
SendMessage () действительно решает эту ситуацию, и я использую эту команду во многих ситуациях, но такая система сообщений без назначения менее эффективна, чем прямая ссылка на получателя. Вы должны быть осторожны, полагаясь на эту команду для обычного общения между объектами.
Джоккинг
2
Я являюсь личным поклонником использования событий и делегатов, где объекты Компонентов знают о более внутренних базовых системах, но базовые системы ничего не знают о компонентах. Но я не был на сто процентов уверен, что именно так они и хотели получить ответ. Поэтому я ответил на вопрос, как полностью отсоединить сценарии.
Бретт Сатору
1
Я также считаю, что в хранилище ресурсов Unity есть несколько плагинов, которые могут предоставлять интерфейсы и другие программные конструкции, не поддерживаемые Unity Inspector, возможно, стоит поискать быстро.
Мэтью Пиграм
7

Я лично НИКОГДА не использую SendMessage. Между вашими компонентами по-прежнему существует зависимость от SendMessage, она очень плохо показана и ее легко сломать. Использование интерфейсов и / или делегатов действительно устраняет необходимость когда-либо использовать SendMessage (что медленнее, хотя это не должно вызывать беспокойства, пока это не нужно).

http://forum.unity3d.com/threads/free-vfw-full-set-of-drawers-savesystem-serialize-interfaces-generics-auto-props-delegates.266165/

Используй вещи этого парня. Он предоставляет кучу редакторов, таких как открытые интерфейсы и делегаты. Есть куча вещей вроде этого, или вы можете даже сделать свой собственный. Что бы вы ни делали, НЕ ставьте под угрозу свой дизайн из-за отсутствия поддержки сценариев в Unity. Эти вещи должны быть в Unity по умолчанию.

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

Бен
источник
2
Лично я опасаюсь стать слишком зависимым от плагинов и библиотек для подобных вещей низкого уровня. Возможно, я немного параноик, но мне кажется, что слишком большой риск того, что мой проект потерпит крах, потому что их код ломается после обновления Unity.
Джоккинг
Я использовал этот фреймворк и в конце концов прекратил работу, так как у меня была причина, по которой @jhocking упоминает о том, что грызет у меня в голове (и это не помогло, что от одного обновления к другому, оно перешло от помощника редактора к системе, включающей все но кухонная раковина, ломая обратную совместимость [и удаляя довольно полезную функцию или две]]
Selali Adobor
6

Подход Unity для меня заключается в том, чтобы сначала GetComponents<MonoBehaviour>()получить список сценариев, а затем преобразовать эти сценарии в частные переменные для определенных интерфейсов. Вы хотите сделать это в Start () в скрипте инжектора зависимостей. Что-то типа:

MonoBehaviour[] list = GetComponents<MonoBehaviour>();
for (int i = 0; i < list.Length; i++) {
    if (list[i] is IHealth) {
        Debug.Log("Found it!");
    }
}

Фактически, с помощью этого метода вы могли бы даже заставить отдельные компоненты сообщать сценарию внедрения зависимостей, каковы их зависимости, используя команду typeof () и тип «Тип» . Другими словами, компоненты предоставляют инжектору зависимостей список типов, а затем инжектор зависимостей возвращает список объектов этих типов.


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

jhocking
источник
Я не могу наследовать от нескольких классов, в то время как я могу реализовать несколько интерфейсов. Это может быть проблемой, но я думаю, что это намного лучше, чем ничего :) Спасибо за ваш ответ!
KL
Что касается редактирования - как только у меня есть список сценариев, как мой код узнает, какой сценарий нельзя подключить к какому интерфейсу?
KL
1
отредактировано, чтобы объяснить это тоже
jhocking
1
В Unity 5 вы можете использовать GetComponent<IMyInterface>()и GetComponents<IMyInterface>()напрямую, который возвращает компонент (ы), который его реализует.
С. Тарык Четин
5

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

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

sdabet
источник
2

Взгляните на IUnified ( http://u3d.as/content/wounded-wolf/iunified/5H1 ), который позволяет вам предоставлять интерфейсы в редакторе, как вы упомянули. По сравнению с обычным объявлением переменных нужно сделать немного больше, вот и все.

public interface IPinballValue{
   void Add(int value);
}

[System.Serializable]
public class IPinballValueContainer : IUnifiedContainer<IPinballValue> {}

public class MyClassThatExposesAnInterfaceProperty : MonoBehaviour {
   [SerializeField]
   IPinballValueContainer value;
}

Кроме того, как уже упоминалось в других ответах, использование SendMessage не удаляет связь. Вместо этого он делает это не ошибка во время компиляции. Очень плохо - когда вы расширяете свой проект, его обслуживание может стать кошмаром.

Если вы также хотите отделить свой код без использования интерфейса в редакторе, вы можете использовать строго типизированную систему, основанную на событиях (или даже на самом деле слабо типизированную, но, опять же, сложную в обслуживании). Я основал свой пост на этом посте , который очень удобен в качестве отправной точки. Будьте внимательны, создавая кучу событий, так как это может вызвать GC. Вместо этого я решил проблему с пулом объектов, но в основном потому, что я работаю с мобильными устройствами.

АБР
источник
1

Источник: http://www.udellgames.com/posts/ultra-useful-unity-snippet-developers-use-interfaces/

[SerializeField]
private GameObject privateGameObjectName;

public GameObject PublicGameObjectName
{
    get { return privateGameObjectName; }
    set
    {
        //Make sure changes to privateGameObjectName ripple to privateInterfaceName too
        if (privateGameObjectName != value)
        {
            privateInterfaceName = null;
            privateGameObjectName = value;
        }
    }
}

private InterfaceType privateInterfaceName;
public InterfaceType PublicInterfaceName
{
    get
    {
        if (privateInterfaceName == null)
        {
            privateInterfaceName = PublicGameObjectName.GetComponent(typeof(InterfaceType)) as InterfaceType;
        }
        return privateInterfaceName;
    }
    set
    {
        //Make sure changes to PublicInterfaceName ripple to PublicGameObjectName too
        if (privateInterfaceName != value)
        {
            privateGameObjectName = (privateInterfaceName as Component).gameObject;
            privateInterfaceName = value;
        }

    }
}
Луи Хонг
источник
0

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

Одна из тех, о которых я не упомянул, - это использование MonoBehaviorдоступа к различным реализациям данного интерфейса. Например, у меня есть интерфейс, который определяет, как реализованы Raycasts, названныйIRaycastStrategy

public interface IRaycastStrategy 
{
    bool Cast(Ray ray, out RaycastHit raycastHit, float distance, LayerMask layerMask);
}

Затем я реализую его в разных классах с такими классами, как LinecastRaycastStrategyи, SphereRaycastStrategyи реализую MonoBehavior, RaycastStrategySelectorкоторый предоставляет один экземпляр каждого типа. Нечто подобное RaycastStrategySelectionBehavior, в котором реализовано что-то вроде:

public class RaycastStrategySelectionBehavior : MonoBehavior
{

    private readonly Dictionary<RaycastStrategyType, IRaycastStrategy> _raycastStrategies
        = new Dictionary<RaycastStrategyType, IRaycastStrategy>
        {
            { RaycastStrategyType.Line, new LineRaycastStrategy() },
            { RaycastStrategyType.Sphere, new SphereRaycastStrategy() }
        };

    public RaycastStrategyType StrategyType;


    public IRaycastStrategy RaycastStrategy
    {
        get
        {
            IRaycastStrategy raycastStrategy;
            if (!_raycastStrategies.TryGetValue(StrategyType, out raycastStrategy))
            {
                throw new InvalidOperationException("There is no registered implementation for the selected strategy");
            }
            return raycastStrategy;
        }
    }
}

Если реализации могут ссылаться на функции Unity в своих конструкциях (или просто быть в безопасности, я просто забыл об этом при наборе этого примера) использовать, Awakeчтобы избежать проблем с вызовом конструкторов или обращением к функциям Unity в редакторе или вне основного нить

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

Я использую эту стратегию из-за гибкости, которую она дает вам, основываясь на MonoBehaviors, фактически не заставляя вас реализовывать интерфейсы с использованием MonoBehaviors. Это дает вам «лучшее из обоих миров», поскольку доступ к Селектору можно получить с помощью GetComponent, все сериализации обрабатываются Unity, и Селектор может применять логику во время выполнения для автоматического переключения реализаций на основе определенных факторов.

Селали Адобор
источник