Wie kann ich eine enge Skriptkopplung in Unity vermeiden?

24

Vor einiger Zeit habe ich angefangen, mit Unity zu arbeiten, und ich habe immer noch Probleme mit eng gekoppelten Skripten. Wie kann ich meinen Code strukturieren, um dieses Problem zu vermeiden?

Beispielsweise:

Ich möchte Gesundheits- und Todessysteme in separaten Skripten haben. Ich möchte auch verschiedene austauschbare Laufskripte haben, mit denen ich die Art und Weise ändern kann, in der der Spielercharakter bewegt wird (physikbasierte Trägheitskontrollen wie in Mario im Vergleich zu engen, zuckenden Kontrollen wie in Super Meat Boy). Das Health-Skript muss einen Verweis auf das Death-Skript enthalten, damit es die Die () -Methode auslösen kann, wenn die Gesundheit des Spielers 0 erreicht. Das Death-Skript sollte einen Verweis auf das verwendete Laufskript enthalten, um das Laufen bei Tod zu deaktivieren (I) bin müde von Zombies).

Ich würde normalerweise Schnittstellen schaffen, wie IWalking, IHealthund IDeath, so dass ich , ohne zu brechen , den Rest meines Code diese Elemente in einer Laune heraus ändern kann. Ich würde sie beispielsweise durch ein separates Skript auf dem Player-Objekt einrichten lassen PlayerScriptDependancyInjector. Vielleicht ist das Skript würde öffentlich IWalking, IHealthund IDeathAttribute, so dass die Abhängigkeiten von den Level - Designern aus dem Inspektoren durch Ziehen und Ablegen entsprechende Scripts eingestellt werden können.

Das würde es mir ermöglichen, Spielobjekten einfach Verhaltensweisen hinzuzufügen und mich nicht um fest codierte Abhängigkeiten zu sorgen.

Das Problem in der Einheit

Das Problem ist, dass ich in Unity keine Schnittstellen im Inspector verfügbar machen kann. Wenn ich eigene Inspectors schreibe, werden die Referenzen nicht serialisiert, und es ist eine Menge unnötiger Arbeit. Aus diesem Grund muss ich nur noch eng gekoppelten Code schreiben. Mein DeathSkript macht einen Verweis auf ein InertiveWalkingSkript verfügbar. Aber wenn ich beschließe, dass der Spielercharakter genau kontrolliert, kann ich das TightWalkingSkript nicht einfach ziehen und ablegen, sondern muss das DeathSkript ändern . Das ist Scheiße. Ich kann damit umgehen, aber meine Seele weint jedes Mal, wenn ich so etwas tue.

Was ist die bevorzugte Alternative zu Schnittstellen in Unity? Wie behebe ich dieses Problem? Ich fand diese , aber es sagt mir , was ich schon weiß, und es tut mir nicht sagen , wie das in der Einheit zu tun! Dies beschreibt auch , was getan werden soll, nicht , wie, es ist nicht das Problem der engen Kopplung zwischen Skripten nicht ansprechen.

Alles in allem bin ich der Meinung, dass diese für Leute geschrieben wurden, die zu Unity gekommen sind, mit einem Hintergrund im Spieledesign, der nur das Codieren lernt, und für normale Entwickler gibt es nur sehr wenige Ressourcen zu Unity. Gibt es eine Standardmethode, um Ihren Code in Unity zu strukturieren, oder muss ich meine eigene Methode herausfinden?

KL
quelle
1
The problem is that in Unity I can't expose interfaces in the inspectorIch glaube nicht, dass ich verstehe, was Sie unter "Schnittstelle im Inspektor anzeigen" verstehen, weil mein erster Gedanke "Warum nicht?" War.
jhocking
@jhocking frage die Unity Devs. Sie sehen es einfach nicht im Inspektor. Es wird dort nur dann nicht angezeigt, wenn Sie es als öffentliches Attribut festlegen, und alle anderen Attribute tun dies.
KL,
1
ohhh du meinst, die Schnittstelle als Typ der Variablen zu verwenden und nicht nur ein Objekt mit dieser Schnittstelle zu referenzieren.
jhocking
1
Haben Sie den Original-ECS-Artikel gelesen? cowboyprogramming.com/2007/01/05/evolve-your-heirachy Was ist mit SOLID Design? en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29
jzx
1
@jzx Ich kenne und verwende SOLID Design und bin ein großer Fan von Robert C. Martins Büchern, aber diese Frage dreht sich nur darum, wie man das in Unity macht. Und nein, ich habe diesen Artikel nicht gelesen und lese ihn gerade, danke :)
KL

Antworten:

14

Es gibt verschiedene Möglichkeiten, um eine enge Skriptkopplung zu vermeiden. Internal to Unity ist eine SendMessage-Funktion, die, wenn sie auf das GameObject eines Monobehaviour abzielt, diese Nachricht an alle Objekte im Spiel sendet. Vielleicht haben Sie so etwas in Ihrem Gesundheitsobjekt:

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

Dies ist wirklich vereinfacht, sollte aber genau zeigen, worauf ich hinaus will. Die Eigenschaft, die die Nachricht wie ein Ereignis auslöst. Ihr Todesskript würde dann diese Nachricht abfangen (und wenn nichts vorhanden ist, um sie abzufangen, garantiert SendMessageOptions.DontRequireReceiver, dass Sie keine Ausnahme erhalten) und Aktionen basierend auf dem neuen Integritätswert ausführen, nachdem er festgelegt wurde. Wie so:

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

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

Es besteht jedoch keine Bestellgarantie. Meiner Erfahrung nach geht es bei einem GameObject immer vom obersten zum untersten Skript, aber ich würde mich nicht auf etwas verlassen, das Sie nicht selbst beobachten können.

Es gibt andere Lösungen, die Sie untersuchen könnten, indem Sie eine benutzerdefinierte GameObject-Funktion erstellen, die Komponenten abruft und nur diejenigen Komponenten zurückgibt, die eine bestimmte Schnittstelle anstelle von Type haben. Dies würde etwas länger dauern, könnte aber Ihren Zwecken gut dienen.

Darüber hinaus können Sie einen benutzerdefinierten Editor schreiben, der ein Objekt prüft, das einer Position im Editor für diese Schnittstelle zugewiesen ist, bevor Sie die Zuweisung tatsächlich durchführen (in diesem Fall würden Sie das Skript wie gewohnt zuweisen, damit es serialisiert werden kann, aber einen Fehler auslösen wenn es nicht die richtige Schnittstelle verwendet und die Zuordnung verweigert). Ich habe beides schon einmal gemacht und kann diesen Code produzieren, brauche aber etwas Zeit, um ihn zuerst zu finden.

Brett Satoru
quelle
5
SendMessage () behebt diese Situation und ich verwende diesen Befehl in vielen Situationen, aber ein solches ungezieltes Nachrichtensystem ist weniger effizient als eine direkte Referenz zum Empfänger. Sie sollten darauf achten, diesen Befehl für die gesamte Routinekommunikation zwischen Objekten zu verwenden.
jhocking
2
Ich bin ein persönlicher Fan der Verwendung von Ereignissen und Delegaten, bei denen Komponentenobjekte mehr über die internen Kernsysteme wissen, die Kernsysteme jedoch nichts über die Komponententeile wissen. Aber ich war mir nicht hundertprozentig sicher, wie sie ihre Antwort haben wollten. Also ging ich mit etwas, das antwortete, wie man Skripte vollständig entkoppelt.
Brett Satoru
1
Ich glaube auch, dass es im Unity Asset Store einige Plugins gibt, die Schnittstellen und andere Programmierkonstrukte verfügbar machen können, die vom Unity Inspector nicht unterstützt werden. Eine schnelle Suche könnte sich lohnen.
Matthew Pigram
7

Ich persönlich verwende SendMessage NIEMALS. Es gibt immer noch eine Abhängigkeit zwischen Ihren Komponenten mit SendMessage, es ist nur sehr schlecht dargestellt und leicht zu brechen. Durch die Verwendung von Schnittstellen und / oder Delegaten entfällt die Notwendigkeit, SendMessage jemals zu verwenden (was langsamer ist, obwohl dies erst dann ein Problem sein sollte, wenn es erforderlich ist).

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

Verwenden Sie das Zeug dieses Kerls. Es bietet einen Haufen von Editor-Dingen wie offen gelegte Schnittstellen und Stellvertreter. Es gibt jede Menge Sachen da draußen, oder Sie können sogar Ihre eigenen machen. Beeinträchtigen Sie auf keinen Fall Ihr Design, da Unity keine Skriptunterstützung bietet. Diese Dinge sollten standardmäßig in Unity sein.

Wenn dies keine Option ist, ziehen Sie stattdessen Monobehaviour-Klassen per Drag & Drop und wandeln Sie sie dynamisch in die erforderliche Schnittstelle um. Es ist mehr Arbeit und weniger elegant, aber es erledigt die Arbeit.

Ben
quelle
2
Persönlich bin ich misstrauisch, zu sehr von Plugins und Bibliotheken abhängig zu werden, wenn es um solche Dinge auf niedriger Ebene geht. Ich bin wahrscheinlich ein bisschen paranoid, aber es scheint zu riskant, dass mein Projekt kaputt geht, weil der Code nach einem Unity-Update kaputt geht.
jhocking
Ich benutzte dieses Framework und hörte schließlich auf, da ich den Grund hatte, warum @jhocking-Erwähnungen im Hinterkopf nagten (und es half nicht, dass es von einem Update zum nächsten ging, von einem Editor-Helfer zu einem System, das alles beinhaltete aber die Küchenspüle, während Abwärtskompatibilität zu brechen [und ein ziemlich nützliches Feature oder zwei entfernen])
Selali Adobor
6

Ein Unity-spezifischer Ansatz, der mir einfällt, wäre, zunächst GetComponents<MonoBehaviour>()eine Liste von Skripten abzurufen und diese Skripten dann in private Variablen für die spezifischen Schnittstellen umzuwandeln. Sie möchten dies in Start () im Abhängigkeitsinjektorskript tun. So etwas wie:

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

Mit dieser Methode können Sie sogar festlegen, dass die einzelnen Komponenten dem Abhängigkeitsinjektionsskript ihre Abhängigkeiten mitteilen, indem Sie den Befehl typeof () und den Typ 'Type' verwenden . Mit anderen Worten, die Komponenten geben dem Abhängigkeitsinjektor eine Liste von Typen und dann gibt der Abhängigkeitsinjektor eine Liste von Objekten dieser Typen zurück.


Alternativ können Sie anstelle von Schnittstellen auch abstrakte Klassen verwenden. Eine abstrakte Klasse kann so ziemlich den gleichen Zweck erfüllen wie eine Schnittstelle, auf den Sie im Inspector verweisen können.

jhocking
quelle
Ich kann nicht von mehreren Klassen erben, während ich mehrere Schnittstellen implementieren kann. Das könnte ein Problem sein, aber ich denke, es ist viel besser als nichts :) Danke für deine Antwort!
KL,
Was die Bearbeitung angeht: Wenn ich die Liste der Skripte habe, woher weiß mein Code, welches Skript mit welcher Schnittstelle verbunden werden soll?
KL,
1
bearbeitet, um das auch zu erklären
jhocking
1
In der Einheit 5 können Sie verwenden GetComponent<IMyInterface>()und GetComponents<IMyInterface>()direkt whch kehrt die Komponente (n) , dass Geräte es.
S. Tarık Çetin
5

Nach meiner eigenen Erfahrung handelt es sich bei Game Dev traditionell um einen pragmatischeren Ansatz als bei Industrial Dev (mit weniger Abstraktionsebenen). Teilweise, weil Sie die Leistung der Wartbarkeit vorziehen möchten und weil Sie Ihren Code weniger wahrscheinlich in anderen Kontexten wiederverwenden (normalerweise besteht eine starke intrinsische Kopplung zwischen der Grafik und der Spielelogik).

Ich verstehe Ihre Bedenken vollkommen, insbesondere, weil Leistungsprobleme bei neueren Geräten weniger kritisch sind. Ich bin jedoch der Meinung, dass Sie Ihre eigenen Lösungen entwickeln müssen, wenn Sie industrielle OOP-Best Practices für die Entwicklung von Unity-Spielen anwenden möchten (z. B. Abhängigkeitsinjektion, etc...).

sdabet
quelle
2

Schauen Sie sich IUnified ( http://u3d.as/content/wounded-wolf/iunified/5H1 ) an, mit dem Sie Schnittstellen im Editor anzeigen können , wie Sie bereits erwähnt haben. Im Vergleich zur normalen Variablendeklaration ist etwas mehr Verdrahtung zu erledigen, das ist alles.

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

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

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

Wie bereits erwähnt, wird die Kopplung durch die Verwendung von SendMessage nicht entfernt. Stattdessen wird beim Kompilieren kein Fehler ausgegeben. Sehr schlecht - wenn Sie Ihr Projekt vergrößern, kann es zu einem Albtraum werden.

Wenn Sie Ihren Code weiter entkoppeln möchten, ohne die Schnittstelle im Editor zu verwenden, können Sie ein stark typisiertes ereignisbasiertes System verwenden (oder sogar ein locker typisiertes System, das jedoch schwieriger zu warten ist). Ich habe meinen auf diesen Beitrag gestützt , der als Ausgangspunkt super praktisch ist. Denken Sie daran, eine Reihe von Ereignissen zu erstellen, da dies die GC auslösen könnte. Ich habe das Problem stattdessen mit einem Objektpool umgangen, aber das liegt hauptsächlich daran, dass ich mit Mobilgeräten arbeite.

ADB
quelle
1

Quelle: 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
quelle
0

Ich benutze ein paar verschiedene Strategien, um die von Ihnen genannten Einschränkungen zu umgehen.

Eine der nicht erwähnten MonoBehaviorMethoden ist die Verwendung einer , die den Zugriff auf die verschiedenen Implementierungen einer bestimmten Schnittstelle ermöglicht. Ich habe zum Beispiel eine Schnittstelle, die definiert, wie Raycasts implementiert werdenIRaycastStrategy

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

Ich implementiere es dann in verschiedenen Klassen mit Klassen wie LinecastRaycastStrategyund SphereRaycastStrategyund implementiere ein MonoBehavior RaycastStrategySelector, das eine einzelne Instanz jedes Typs verfügbar macht. So etwas wie RaycastStrategySelectionBehaviordas implementiert ist:

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

Wenn die Implementierungen wahrscheinlich auf Unity-Funktionen in ihren Konstruktionen verweisen (oder nur auf der sicheren Seite sind), verwenden Sie diese Option Awake, um Probleme beim Aufrufen von Konstruktoren oder beim Verweisen auf Unity-Funktionen im Editor oder außerhalb des Hauptfensters zu vermeiden Faden

Diese Strategie hat einige Nachteile, insbesondere wenn Sie viele verschiedene Implementierungen verwalten müssen, die sich schnell ändern. Probleme mit der fehlenden Überprüfung der Kompilierungszeit können mithilfe eines benutzerdefinierten Editors behoben werden, da der Enum-Wert ohne besonderen Aufwand serialisiert werden kann (obwohl ich normalerweise darauf verzichte, da meine Implementierungen nicht so zahlreich sind).

Ich verwende diese Strategie aufgrund der Flexibilität, die sie Ihnen bietet, wenn Sie auf MonoBehaviors basieren, ohne dass Sie gezwungen sind, die Schnittstellen mithilfe von MonoBehaviors zu implementieren. Dies gibt Ihnen "das Beste aus zwei Welten", da auf den Selector über zugegriffen werden kann GetComponent, die Serialisierung von Unity übernommen wird und der Selector zur Laufzeit Logik anwenden kann, um Implementierungen basierend auf bestimmten Faktoren automatisch umzuschalten.

Selali Adobor
quelle