Як я можу уникнути жорсткого з'єднання сценаріїв в Unity?


24

Деякий час тому я почав працювати з Unity, і я все ще борюся з питанням щільно з'єднаних сценаріїв. Як я можу структурувати свій код, щоб уникнути цієї проблеми?

Наприклад:

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

Я зазвичай створювати інтерфейси, як IWalking, IHealthі IDeath, таким чином , що я можу змінити на натхненням ці елементи , не порушуючи решту мого коду. Скажімо, я б їх створив окремим сценарієм на об’єкті програвача PlayerScriptDependancyInjector. Можливо , що сценарій буде мати громадські IWalking, IHealthі IDeathатрибути, так що залежно може бути встановлений конструктором рівня від інспектора, перетягуючи відповідні сценарії.

Це дозволило б мені легко додавати поведінку до ігрових об'єктів і не турбуватися про важко кодовані залежності.

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

Проблема в тому , що в Unity я не можу виставити інтерфейси в інспектора, і якщо я пишу свої власні інспектори, посилання не отримаєте серіалізовать, і це дуже багато непотрібної роботи. Тому мені залишається писати щільно зв'язаний код. Мій Deathсценарій розкриває посилання на InertiveWalkingсценарій. Але якщо я вирішу, що хочу, щоб персонаж гравця жорстко контролював, я не можу просто перетягнути TightWalkingсценарій, мені потрібно змінити Deathсценарій. Це смокче. Я можу з цим впоратися, але моя душа плаче кожен раз, коли я роблю щось подібне.

Яка краща альтернатива інтерфейсам в Unity? Як виправити цю проблему? Я це знайшов , але він говорить мені те, що я вже знаю, і не говорить мені, як це зробити в Єдності! Це теж обговорює, що слід робити, а не як, це не стосується питання тісної зв'язку між сценаріями.

Взагалі я відчуваю, що вони написані для людей, які приїхали в Unity з ігровим дизайном, який просто навчився кодувати, і в Unity дуже мало ресурсів для звичайних розробників. Чи є якийсь стандартний спосіб структурувати свій код в Unity, або я повинен з'ясувати свій власний метод?


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 дизайн, і я великий фанат книг Роберта К. Мартінса, але це питання стосується того, як це зробити в Unity. І ні, я не читав цю статтю, читаючи її зараз, дякую :)
KL

Відповіді:


14

Існує кілька способів роботи, щоб уникнути жорсткого з'єднання сценарію. Internal to Unity - функція SendMessage, яка при націлюванні на GameObject Monobehaviour 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 () та тип 'Type' . Іншими словами, компоненти надають інжектору залежності список типів, а потім інжектор залежності повертає список об'єктів цих типів.


Крім того, ви можете просто використовувати абстрактні класи замість інтерфейсів. Абстрактний клас може досягти майже тієї ж мети, що і інтерфейс, і ви можете посилатися на це в Інспекторі.


Я не можу успадковувати з декількох класів, хоча я можу реалізувати декілька інтерфейсів. Це може бути проблемою, але я думаю, це набагато краще, ніж нічого :) Дякую за вашу відповідь!
KL

що стосується редагування - як тільки у мене є список скриптів, як мій код знає, який скрипт не піддається інтерфейсу?
KL

1
відредаговано так, щоб пояснити, що це
поштовх

1
У Unity 5 ви можете використовувати GetComponent<IMyInterface>()та GetComponents<IMyInterface>()безпосередньо, що повертає компонент (и), які його реалізують.
S. Tarık Çetin

5

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

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


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, і Селектор може застосовувати логіку під час виконання, щоб автоматично перемикати реалізації на основі певних факторів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.