Jak mogę uniknąć ścisłego łączenia skryptów w Unity?

24

Jakiś czas temu zacząłem pracować z Unity i nadal mam problem z ściśle powiązanymi skryptami. Jak mogę uporządkować kod, aby uniknąć tego problemu?

Na przykład:

Chcę mieć systemy zdrowia i śmierci w osobnych skryptach. Chcę też mieć różne wymienne skrypty chodzenia, które pozwalają mi zmieniać sposób poruszania się postaci gracza (oparte na fizyce, bezwładnościowe elementy sterujące, takie jak w Mario, w porównaniu do ścisłych, drżących elementów sterujących, takich jak w Super Meat Boy). Skrypt zdrowia musi zawierać odniesienie do skryptu śmierci, aby mógł uruchomić metodę Die (), gdy zdrowie graczy osiągnie 0. Skrypt śmierci powinien zawierać odniesienie do użytego skryptu chodzenia, aby wyłączyć chodzenie po śmierci (I mam dość zombie).

Chciałbym normalnie tworzyć interfejsy, jak IWalking, IHealthi IDeath, tak, że można zmienić te elementy na kaprysu bez rozbijania resztę kodu. Powiedzmy, że ustawiłbym je osobnym skryptem na obiekcie odtwarzacza PlayerScriptDependancyInjector. Może to skrypt musiałby publicznych IWalking, IHealthoraz IDeathatrybuty, tak że zależności może być ustawiona przez projektanta z poziomu inspektora przez przeciąganie i upuszczanie odpowiednich skryptów.

To pozwoliłoby mi po prostu łatwo dodawać zachowania do obiektów gry i nie martwić się o mocno zakodowane zależności.

Problem w jedności

Problemem jest to, że w jedności nie mogę wystawiać interfejsów w inspektora, a jeśli piszę własnych inspektorów, odniesienia przyzwyczajenie się w odcinkach, a to dużo niepotrzebnej pracy. Dlatego pozostało mi pisanie ściśle powiązanego kodu. Mój Deathskrypt ujawnia odniesienie do InertiveWalkingskryptu. Ale jeśli zdecyduję, że chcę, aby postać gracza ściśle kontrolowała, nie mogę po prostu przeciągnąć i upuścić TightWalkingskryptu, muszę go zmienić Death. To jest do bani. Poradzę sobie z tym, ale moja dusza płacze za każdym razem, gdy robię coś takiego.

Jaka jest preferowana alternatywa dla interfejsów w Unity? Jak rozwiązać ten problem? Znalazłem to , ale mówi mi to, co już wiem, i nie mówi mi, jak to zrobić w Jedności! To również omawia, co należy zrobić, a nie jak, nie rozwiązuje problemu ścisłego łączenia skryptów.

Podsumowując, uważam, że zostały napisane dla osób, które przyszły do ​​Unity z doświadczeniem w projektowaniu gier, które po prostu uczą się kodować, a dla Unity jest bardzo mało zasobów dla zwykłych programistów. Czy istnieje jakiś standardowy sposób na uporządkowanie kodu w Unity, czy też muszę wymyślić własną metodę?

KL
źródło
1
The problem is that in Unity I can't expose interfaces in the inspectorNie sądzę, że rozumiem, co masz na myśli przez „ujawnienie interfejsu w inspektorze”, ponieważ moją pierwszą myślą było „dlaczego nie?”
jhocking
@jhocking zapytaj deweloperów jedności. Po prostu nie widzisz tego w inspektorze. Po prostu nie pojawia się, jeśli ustawisz go jako atrybut publiczny, a wszystkie inne atrybuty tak.
KL
1
ohhh masz na myśli użycie interfejsu jako typu zmiennej, a nie tylko odwołanie do obiektu za pomocą tego interfejsu.
jhocking
1
Czy przeczytałeś oryginalny artykuł ECS? cowboyprogramming.com/2007/01/05/evolve-your-heirachy Co z projektem SOLID? en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29
jzx
1
@ jzx Wiem i używam SOLID design i jestem wielkim fanem książek Roberta C. Martinsa, ale to pytanie dotyczy tego, jak to zrobić w Unity. I nie, nie przeczytałem tego artykułu, czytając go teraz, dzięki :)
KL

Odpowiedzi:

14

Istnieje kilka sposobów pracy, aby uniknąć ścisłego łączenia skryptów. Internal to Unity to funkcja SendMessage, która po skierowaniu na obiekt GameObject Monobehaviour wysyła tę wiadomość do wszystkiego w obiekcie gry. Więc możesz mieć coś takiego w swoim obiekcie zdrowia:

[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;
}

To jest naprawdę uproszczone, ale powinno dokładnie pokazać, o co mi chodzi. Właściwość rzucająca wiadomość jak zdarzenie. Twój skrypt śmierci przechwyciłby następnie tę wiadomość (a jeśli nie ma nic, co mogłoby ją przechwycić, SendMessageOptions.DontRequireReceiver zagwarantuje, że nie otrzymasz wyjątku) i wykonał działania w oparciu o nową wartość zdrowia po jej ustawieniu. Tak jak:

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

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

Nie ma w tym jednak żadnej gwarancji porządku. Z mojego doświadczenia wynika, że ​​zawsze przebiega od najwyższego skryptu GameObject do najniższego skryptu, ale nie opierałbym się na niczym, czego sam nie mógłbyś zaobserwować.

Istnieją inne rozwiązania, które można zbadać, które budują niestandardową funkcję GameObject, która pobiera Komponenty i zwraca tylko te komponenty, które mają określony interfejs zamiast Typu, zajęłoby to trochę dłużej, ale mogłoby dobrze służyć twoim celom.

Dodatkowo możesz napisać niestandardowy edytor, który sprawdza obiekt przypisany do pozycji w edytorze dla tego interfejsu przed faktycznym wykonaniem przypisania (w tym przypadku skrypt byłby przypisany normalnie, umożliwiając serializację, ale generowanie błędu jeśli nie używał właściwego interfejsu i odmawiał przypisania). Zrobiłem już oba z nich i mogę wygenerować ten kod, ale najpierw potrzebuję trochę czasu, aby go znaleźć.

Brett Satoru
źródło
5
SendMessage () rozwiązuje tę sytuację i używam tego polecenia w wielu sytuacjach, ale taki nieukierowany system komunikatów jest mniej wydajny niż bezpośrednie odniesienie do odbiornika. Należy zachować ostrożność, polegając na tym poleceniu w przypadku każdej rutynowej komunikacji między obiektami.
jhocking
2
Jestem osobistym fanem wykorzystania wydarzeń i delegatów, w których obiekty Component wiedzą o więcej wewnętrznych systemach rdzeniowych, ale systemy podstawowe nie wiedzą nic o elementach składowych. Ale nie byłem w stu procentach pewien, że tak właśnie chcieli zaprojektować swoją odpowiedź. Poszedłem więc z czymś, co odpowiedziało, jak całkowicie odłączyć skrypty.
Brett Satoru,
1
Uważam również, że w sklepie zasobów Unity dostępne są niektóre wtyczki, które mogą ujawniać interfejsy i inne konstrukcje programowe nieobsługiwane przez Unity Inspector, być może warto mieć szybkie wyszukiwanie.
Matthew Pigram
7

Ja osobiście NIGDY nie korzystam z SendMessage. Nadal istnieje zależność między twoimi komponentami dzięki SendMessage, jest ona bardzo słabo wyświetlana i łatwa do złamania. Korzystanie z interfejsów i / lub delegatów naprawdę eliminuje potrzebę korzystania z SendMessage w dowolnym momencie (co jest wolniejsze, chociaż nie powinno to stanowić problemu, dopóki nie będzie konieczne).

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

Użyj rzeczy tego faceta. Zapewnia mnóstwo edytorów, takich jak odsłonięte interfejsy ORAZ delegaci. Istnieje mnóstwo takich rzeczy, a nawet możesz stworzyć własne. Cokolwiek robisz, NIE kompromituj swojego projektu ze względu na brak obsługi skryptów Unity. Te rzeczy powinny być domyślnie w Jedności.

Jeśli nie jest to opcja, przeciągnij i upuść klasy monobehaviour zamiast tego i dynamicznie rzutuj je na wymagany interfejs. To więcej pracy i mniej eleganckie, ale wykonuje pracę.

Ben
źródło
2
Osobiście jestem zbyt ostrożny, aby stać się zbyt zależny od wtyczek i bibliotek dla rzeczy niskiego poziomu takich jak ten. Prawdopodobnie jestem trochę paranoikiem, ale wydaje mi się, że istnieje duże ryzyko, że mój projekt się zepsuje, ponieważ ich kod psuje się po aktualizacji Unity.
jhocking
Korzystałem z tego frameworku i ostatecznie przestałem, ponieważ właśnie z tego powodu @jhocking wspomina o gryzeniu z tyłu głowy (i to nie pomogło, że z jednej aktualizacji do drugiej, zmienił się z pomocnika edytora w system, który zawierał wszystko ale zlewozmywak kuchenny łamie kompatybilność wsteczną [i usuwa raczej przydatną funkcję lub dwie])
Selali Adobor
6

Podejście specyficzne dla Jedności, które przychodzi mi do głowy, to najpierw GetComponents<MonoBehaviour>()uzyskać listę skryptów, a następnie rzutować te skrypty na zmienne prywatne dla określonych interfejsów. Chciałbyś to zrobić w Start () na skrypcie inżektora zależności. Coś jak:

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

W rzeczywistości dzięki tej metodzie można nawet kazać poszczególnym komponentom powiedzieć skryptowi wstrzykiwania zależności, jakie są ich zależności, za pomocą polecenia typeof () i typu „Type” . Innymi słowy, komponenty przekazują iniektorowi zależności listę typów, a następnie iniektor zależności zwraca listę obiektów tych typów.


Alternatywnie, możesz po prostu użyć klas abstrakcyjnych zamiast interfejsów. Klasa abstrakcyjna może osiągnąć prawie ten sam cel, co interfejs i można do niej odwoływać się w Inspektorze.

jhocking
źródło
Nie mogę dziedziczyć po wielu klasach, ale mogę zaimplementować wiele interfejsów. To może być problem, ale myślę, że jest o wiele lepszy niż nic :) Dziękuję za odpowiedź!
KL
jeśli chodzi o edycję - skoro mam listę skryptów, skąd mój kod wie, który skrypt nie może zostać przypisany do którego interfejsu?
KL
1
zredagowane również, aby to wyjaśnić
jhocking
1
W Jedności 5, można użyć GetComponent<IMyInterface>()i GetComponents<IMyInterface>()bezpośrednio whch powraca składnik (ów) Realizuje.
S. Tarık Çetin
5

Z własnego doświadczenia twórcy gier tradycyjnie wymagają bardziej pragmatycznego podejścia niż twórcy przemysłowi (z mniejszą ilością warstw abstrakcji). Częściowo dlatego, że chcesz przedkładać wydajność nad łatwość konserwacji, a także dlatego, że mniej prawdopodobne jest ponowne użycie kodu w innych kontekstach (zwykle istnieje silne wewnętrzne powiązanie między grafiką a logiką gry)

Całkowicie rozumiem twoje obawy, szczególnie dlatego, że problemy z wydajnością są mniej krytyczne w przypadku najnowszych urządzeń, ale czuję, że będziesz musiał opracować własne rozwiązania, jeśli chcesz zastosować najlepsze praktyki OOP w rozwoju gier Unity (takie jak wstrzykiwanie zależności, itp...).

sdabet
źródło
2

Spójrz na IUnified ( http://u3d.as/content/wounded-wolf/iunified/5H1 ), który pozwala ujawniać interfejsy w edytorze, tak jak wspomniałeś. Jest trochę więcej do zrobienia w porównaniu do normalnej deklaracji zmiennych, to wszystko.

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

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

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

Ponadto, jak wspomniano w innych odpowiedziach, użycie SendMessage nie usuwa sprzężenia. Zamiast tego sprawia, że ​​nie jest to błąd podczas kompilacji. Bardzo źle - w miarę zwiększania skali projektu utrzymanie go może stać się koszmarem.

Jeśli chcesz rozdzielić kod bez korzystania z interfejsu w edytorze, możesz użyć silnie typowanego systemu opartego na zdarzeniach (lub nawet luźno typowanego, ale trudniejszego w utrzymaniu). Oparłem mój na tym poście , co jest bardzo wygodne jako punkt wyjścia. Pamiętaj, aby stworzyć mnóstwo wydarzeń, ponieważ mogą one wywołać GC. Zamiast tego obejrzałem problem z pulą obiektów, ale głównie dlatego, że pracuję z urządzeniami mobilnymi.

ADB
źródło
1

Źródło: 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;
        }

    }
}
Louis Hong
źródło
0

Używam kilku różnych strategii, aby ominąć wspomniane ograniczenia.

Jednym z tych, o których nie widzę, jest użycie MonoBehavior które udostępnia dostęp do różnych implementacji danego interfejsu. Na przykład mam interfejs, który określa sposób implementacji nazw RaycastIRaycastStrategy

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

I następnie wdrożyć go w różnych klas z klas, jak LinecastRaycastStrategyi SphereRaycastStrategyi realizuje MonoBehaviorRaycastStrategySelector że odsłania jedno wystąpienie każdego typu. Coś podobnego RaycastStrategySelectionBehavior, co jest realizowane coś takiego:

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;
        }
    }
}

Jeśli implementacje prawdopodobnie odwołują się do funkcji Unity w swoich konstrukcjach (lub po prostu są po bezpiecznej stronie, po prostu zapomniałem o tym podczas pisania tego przykładu) używaj, Awakeaby uniknąć problemów z wywoływaniem konstruktorów lub odwoływaniem się do funkcji Unity w edytorze lub poza głównym wątek

Ta strategia ma pewne wady, szczególnie gdy trzeba zarządzać wieloma różnymi implementacjami, które szybko się zmieniają. Problemy z brakiem sprawdzania czasu kompilacji można rozwiązać, korzystając z edytora niestandardowego, ponieważ wartość wyliczenia może być serializowana bez specjalnej pracy (chociaż zwykle rezygnuję z tego, ponieważ moje implementacje nie są tak liczne).

Korzystam z tej strategii ze względu na elastyczność, jaką zapewnia, bazując na MonoBehaviors bez faktycznego zmuszania do implementacji interfejsów przy użyciu MonoBehaviors. To daje „najlepsze z obu światów”, ponieważ dostęp do Selektora można uzyskać za pomocą GetComponent, serializacja jest obsługiwana przez Unity, a Selektor może zastosować logikę w czasie wykonywania, aby automatycznie przełączać implementacje na podstawie pewnych czynników.

Selali Adobor
źródło