Jump to content
Unity Insider Forum

Scripts entkoppeln - Wann ist es sinnvoll?


Bibo

Recommended Posts

Guten Tag zusammen,

ich habe mir vor kurzem ein Tutorial angesehen. Es wurde erwähnt, dass es sinnvoll sei einzelne Scripte voneinander zu entkoppeln, also dafür zu sorgen, dass Scripte nie direkt miteinander kommunizieren sondern über eine Art Manager- Script laufen.

So soll verhindert werden, dass sich ein Fehler in einem Script auf anderen auswirkt und somit Fehler entstehen, welche schwer nachvollziehbar sind. Das klang am Anfang nachvollziehbar, jedoch verstehe ich nicht ganz wo es sinnvoll ist und wo nicht. Vielleicht kann jemand der Erfahrungen mit größeren Projekten hat, mal das eine oder andere Beispiel geben. 

Ich danke euch schon einmal im Voraus.

Gruß Basti

Link zu diesem Kommentar
Auf anderen Seiten teilen

Moin!

Diesen Mumpitz mit den Manager-Scripten sieht man ständig. Meine Theorie ist, dass das so ein Teufelskreis ist. Leute finden das, benutzen es, es funktioniert (und das tut es halt auch) und empfehlen es weiter. Mal anhalten und darüber nachdenken, was das für Vor- und Nachteile hat, tun scheinbar die wenigsten. Ich für meinen Teil kriege jedenfalls fast immer direkt Hautausschlag, wenn ich eine Klasse sehe, die IrgendwasManager heißt.

Ich muss das direkt vorher sagen: Ich komme hier zwar mit vielen Jahren Unity-Erfahrung, aber dennoch mit meiner eigenen Meinung. Ich werde diese Meinung so gut begründen, wie ich kann, aber trotzdem ist es vielleicht nicht die absolute Wahrheit.

Fangen wir mal mit dem Klassennamen an. Wenn du einen "EnemyManager" siehst, weißt du dann, was er macht? Eher nicht. Du weißt, dass es etwas mit den Gegnern zu tun hat. Und du kannst dir schon denken, dass gegnermäßig nicht viel läuft, wenn diese Klasse fehlen würde. Aber... geht es um einen Gegner? Oder um alle Gegner? Geht es um den Zustand des Gegners? Um sein Pathfinding? Sind die Lebenspunkte vom Manager verwaltet? Oder macht das doch jede "Enemy"-Komponente selbst?
Es klingt vielleicht wie pingeliges Gequengel, aber es geht hier um einen ganz zentralen Punkt. Alleine dadurch, dass du das Ding "Manager" nennst, gestehst du von Anfang an, dass du gar nicht klar definiert hast, was alles in diese Klasse reinkommt und was nicht. Man kann stattdessen auch Klassen haben, bei denen schon im Namen klar ist, welche Aufgabe sie haben. Vielleicht ist nicht immer klar, wie diese Aufgabe von der jeweiligen Klasse angegangen wird, aber es worum es überhaupt geht - das ist sichtbar.

Wo du als Beispiel schwer nachvollziehbare Fehler hast... wenn dein Gegner nicht die kürzeste Route von A nach B nimmt, es aber sollte - würdest du dann lieber den EnemyManager durchkämmen oder die "EnemyNavigation"-Klasse?

Wo wir bei dem Punkt sind...

Am 30.1.2022 um 08:56 schrieb Bibo:

So soll verhindert werden, dass sich ein Fehler in einem Script auf anderen auswirkt

Die Begründung ist überaus krumm. Dinge wirken sich ständig auf andere Dinge aus. Wenn du einen Knopf und eine Tür hast, und der Knopf öffnet die Tür, dann wirkt sich der Knopf auf die Tür aus. Ob du da keine, eine oder drölf Klassen zwischen schaltest, ist im Bezug auf Fehler erstmal egal. Wenn du den Knopf drückst und die Tür nicht aufgeht, dann ist der Fehler entweder beim Knopf-Code, beim Tür-Code oder bei irgendeiner Logik dazwischen. Und daran ändert auch eine Manager-Klasse nichts. Was bei diesem Thema viel wichtiger ist, ist das was ich eben schon meinte: Fest verteilte Aufgaben. Wenn du deine Klassen sauber trennst, dann begrenzt du die Anzahl der Klassen, die an einem bestimmten Bug Schuld sein können, auf maximal zwei oder drei.

Was stimmt ist, dass Klassen immer so unabhängig voneinander wie möglich sein sollten. Aber "so ... wie möglich" heißt nicht "gänzlich". Wenn dein Knopf die Tür öffnen soll, dann muss dein Knopf irgendwie der Tür Bescheid sagen. Da führt einfach kein Weg drumherum. Und wenn er, statt direkt mit der Tür zu reden, stattdessen mit einem Tür-Manager redet, dann ist dem Button trotzdem klar, von welcher Tür er etwas will. Wenn du altbacken den Vater deiner Liebsten um ihre Hand bittest, dann weißt du trotzdem, wenn du da heiraten willst. Da macht auch der Vater als Mittelsmann keinen Unterschied.

Witzigerweise widerspricht die Idee einer Manager-Klasse genau dieser Idee, dass Objekte möglichst autonom sein sollten. Denn anstatt dass ein Gegner eigenständig schaut, was er machen will/soll, lässt er es sich von einem anderen Objekt sagen. Oder holt sich zumindest die Erlaubnis.

Ich bin ganz extrem gegen diese Idee,

Am 30.1.2022 um 08:56 schrieb Bibo:

dafür zu sorgen, dass Scripte nie direkt miteinander kommunizieren

Es gibt aber ein, zwei Sachen, die so ähnlich klingen, aber etwas ganz anderes sind und worauf man mal ein Auge werfen kann. Bleiben wir mal bei dem Knopf und der Tür.

Die simpelste Variante, sowas zu bauen, ist vermutlich

public class Button : MonoBehaviour
{
  public Door door;
  
  public void OnPress()
  {
    door.Open();
  }
}

Da merkt man in der Anwendung dann recht schnell, dass das irgendwie doof ist, weil der Knopf nichts anderes machen kann als Türen zu öffnen. Wäre es nicht auch super, wenn der Knopf ein Lichtschalter sein könnte? Vor allem, weil die Klasse vermutlich mehr Code beinhalten dürfte als hier im Beispiel, von wegen schauen ob der Player in der Nähe ist und ob eine Taste gedrückt wird. Ich will da jetzt gar nicht ins Detail gehen weil das ein riesiges Thema ist. Aber es sei gesagt, dass es prima Möglichkeiten gibt, den Button so zu bauen, dass er alles mögliche an/aus/auf/zu machen kann. Es ist vermutlich leicht nachvollziehbar, dass sowas oft sehr sinnvoll ist. Wenn man das tut, verschwindet der Begriff "Door" aus dem "Button"-Code, weil der Code eben nicht mehr auf Türen gemünzt sein soll.

Außerdem gibt es noch die Idee, dass Dinge manchmal miteinander interagieren, dabei aber anonym bleiben sollten. Ein Beispiel: Du hast ein Game Over im Spiel (soll ja mal vorkommen). Verschiedene Dinge können das Game Over auslösen, zum Beispiel Spieler tot, bestimmter NPC tot oder Zeit ist um. Und dann können da verschiedene Dinge passieren: Bildschirm wird schwarz, Highscore wird gespeichert, ein Menü wird angezeigt, solche Dinge. Es wäre irgendwie doof, wenn jetzt der NPC-Code irgendwie wüsste, was ein Game Over-Menü ist, oder? Wenn du irgendwann mal dein Game Over überarbeitest, müsstest du daran denken, deinen NPC-Code anzupassen, damit das NPC-Tod-bedingte Game Over nicht kaputt geht. An solchen und anderen Stellen ist es in der Tat sinnvoll, die beiden Seiten der Angelegenheit strikt voneinander zu trennen. Man könnte z.B. ein Game Over-Event definieren. Dann bringt man dem einen bestimmten NPC bei, das Event auszulösen, und schreibt noch Code, der dafür sorgt, dass im Falle dieses Events das Menü aufgeht. Der NPC und das Menü kennen sich dann nicht.
Du merkst aber übrigens vielleicht, dass dabei nirgendwo ein "Manager" vorkommt. Es braucht dafür keinen NPCManager, keinen GameOverManager und kein MenuManager.

Ich habe in meinem Code niemals auch nur eine "Manager"-Klasse. Meine Klassen haben alle eine Rolle, die durch den Namen zumindest stark angedeutet wird und meine Objekte können immer dann direkt miteinander kommunizieren, wenn nichts dagegen spricht. Da wird dann ganz einfach die Tür in den Inspektor des Buttons gezogen und fertig ist die Kiste.

Das heißt nicht, dass du auf keinen Fall eine Manager-Klasse bauen darfst. Wie deine Architektur aussieht, hängt stark von der Größe des Projekts ab. Auf der Arbeit arbeite ich an einem Live Service-Spiel, das alle zwei Wochen ein Update raushaut. Und in jedem Update ist eigentlich auch mindestens ein neues Feature oder ein bisschen Content drin (nicht nur Bugfixes). Unsere Architektur ist sehr nervig und krass kompliziert, weil uns diese bestimmte Herangehensweise erlaubt, zu testen als gäb's kein Morgen mehr. Unit Tests, Functional Tests, Integration Tests, Automation Tests. Wenn du alle zwei Wochen eine neue Version deines Produkts rausbringst, kannst du nicht ne Woche davon abschneiden um das Leute per Hand testen zu lassen. Wenn irgendwo etwas zerschossen wurde, was vorher funktioniert hat, dann gehen die Alarmglocken an, bevor der Code überhaupt reingemerged wird. Und das ist verdammt wichtig, denn das Spiel ist super komplex. Da kann sehr schnell mal was kaputt gehen, selbst wenn man super gut programmiert. Das krasseste Gegenbeispiel wäre ein Gamejam. 48 Stunden oder eine Woche lang ein Spiel machen und hinterher muss es mehr oder weniger fertig sein. Wenn du da erstmal krasse Architektur reinhaust, hast du am Ende nix zum Vorzeigen. Es geht also darum, wie wartbar dein Projekt sein soll. Willst du einmal fix ein Gamejam-Spiel machen? Oder willst du ein Jahr lang daran arbeiten, es releasen und dann noch ein Jahr Maintenance (Bugfixes und so) hinten ranhängen? Oder willst du das Spiel über ein Jahrzehnt immer wieder updaten und weiterlaufen lassen, bis die Microtransaction-Einnahmen versiegen? Je nachdem, was du da tust, sollte deine Architektur anders aussehen.

Wenn du also im Gamejam quick and dirty einen GameManager baust, weil du so schön schnell erstes Gameplay implementieren kannst, dann nur zu, kein Ding. Wenn du aber etwas länger an einem Projekt arbeitest, dann werden Manager-Klassen (und viele andere Dinge, die Leute so gerne machen) ganz schnell ganz übel für dich. Das ist halt genau wie mit deinem Stuhl: Wenn du sofort an den PC willst, kannst du dich auf nen Pappkarton setzen. Aber wenn du jeden Tag am PC arbeitest, solltest du vielleicht doch ins Möbelhaus und dir einen ergonomischen Bürostuhl besorgen. Wenn du dann aber die erste bemannte Marslandung verpasst, weil du bei Ikea an der Kasse stehst, ist auch kacke.

So oder so kann ich dir sagen: Nein, du brauchst keine Manager-Klasse. Es gibt immer auch einen besseren Weg - versprochen. Hier hast du dazu einen kleinen Einstiegsartikel von mir (einige Beispiele dürften dir jetzt bekannt vorkommen). Und ansonsten gerne fragen. Ich sag mal: Dieses Zeug ist nichts, was man direkt als Anfänger unter die Füße kriegen muss. Da kann man auch nach zehn Jahren Unity noch etwas dazulernen.

Link zu diesem Kommentar
Auf anderen Seiten teilen

  • 4 weeks later...

Vielen dank für deine Antwort. Dass ein Manager nicht überall sinnvoll ist, leuchtet selbst mir ein. Aber jetzt trennen wir uns mal von dem Wort Manager. Ich hatte mal den Ansatz ein Tabletop in Unity umzusetzen. Da fand ich es durchaus sinnvoll, wenn ein Object, alle anderen Objects ansprechen kann. Ein gutes Beispiel ist in einem solchen Tabletop die Zug-Reihenfolge. Geben wir jeden Spieler und jeden Gegner einen Initiative-Wert, der im Spiel durch Buffs oder ähnliches von Runde zu Runde verändert werden kann, finde ich es schon gut wenn ich ein Object habe, welches für alle an der nächsten Runde beteiligen Figuren diesen Wert ermittelt und die Reihenfolge festsetzt.

Ist das nun ein "Manager"? Klar ist kann den so nennen, aber ich Frage mich gerade ob ich das mit dem Manager verstanden habe?

Link zu diesem Kommentar
Auf anderen Seiten teilen

HierDein Beispiel ist ein sehr gutes dafür, dass es manchmal bei zehn Objekten noch ein elftes "Ding" geben muss, dass irgendeine Wahrheit über die zehn Objekte innehat. Deshalb hat Unity auch die Build List, in der alle Szenen geordnet liegen. Es wäre beknackt, an jede Szene eine Zahl dranzuschreiben.

vor einer Stunde schrieb Bibo:

Aber jetzt trennen wir uns mal von dem Wort Manager.

Das ist tatsächlich viel eher der Punkt, als man glauben könnte. Der Name einer Klasse hat Einfluss darauf, was bei der Entwicklung damit passiert und in welche Richtung die Klasse wächst. Bei Methoden und Variablennamen genauso. Und wenn man seine Klasse "Manager" nennt, dann sagt man von Minute eins an, dass das Ding ein Restposten für alles wird, das man sonst nicht untergekriegt hat. Darüber hinaus besagt der Name "Manager", dass die Klasse bzw. dessen Instanz proaktiv irgendetwas macht. Ein Manager ist nunmal kein Nachschlagewerk oder gibt Auskunft am Schalter - er verteilt Aufträge und sagt den anderen, was sie zu tun haben. In der Realität mal mehr oder mal weniger direkt. In Unity heißt das sehr oft, dass der Manager ein MonoBehaviour sein muss, der irgendwie in der Szene herumliegt, weil er auf MonoBehaviour-Events (wie Update) reagiert, um von dort aus proaktiv Anweisungen an andere Objekte zu verteilen.

Deine Initiative-Liste dagegen ist ein Container für Daten. Objekte tragen sich ein und es passiert natürlich Logik beim Einsortieren. Und dann kommt von irgendwo der Befehl "der nächste ist dran" und die Liste delegiert den "du bist dran"-Auftrag an den ersten in der Schlange. Hier passiert aber kein eigenständiges Erstellen von Aufträgen. Und die Liste hat ganz klar einen Zweck. Du kannst sie also InitiativeQueue nennen oder so. Da sieht man beim Namen schon ziemlich gut, was sie macht. Und die Gefahr, dass man das verwässert und noch etwas anderes einbaut, ist recht gering.

vor einer Stunde schrieb Bibo:

Ist das nun ein "Manager"? Klar ist kann den so nennen, aber ich Frage mich gerade ob ich das mit dem Manager verstanden habe?

Was ein "Manager" ist, ist leider nicht ganz klar definiert. Ist auch noch so ein Problem. Wenn ich auf Manager abmeckere, dann berufe ich mich dabei auf einen Haufen Klassen namens XY-Manager, die ich über die Jahre sehen musste. Und jede einzelne davon war fürchterlich.

In den meisten Fällen war da Code drin, der keinen Bedarf dafür hat, in einer Komponente zu stecken. Er hätte auch einfach static sein können. Damit wäre er dann auch unabhängig davon, ob irgendwo eine Instanz rumhängt. Hier erklärt der gute Herr ganz gut, wo da das Problem ist.

Sehr oft werden Manager halt auch benutzt, um den Objekten Eigenständigkeit zu nehmen. Anstatt das Objekt sich selbst bewegen zu lassen, hat der Manager eine Referenz darauf und kümmert sich dann um die Bewegung. Bei ECS macht man grundsätzlich alles quasi so und da ist das auch gut so, aber dort hat man davon auch Vorteile. Hier ist das einfach eine weitere Verbindung in dem Netzwerk, das du über Abhängigkeiten und Referenzen in dein Projekt baust. Und je verworrener dieses Netz ist, desto schwerer wird es, damit zu arbeiten.

Dein Post am Eingang war ja über diese Aussage:

Am 30.1.2022 um 08:56 schrieb Bibo:

Scripte nie direkt miteinander kommunizieren sondern über eine Art Manager- Script laufen.

Und ob du das jetzt Manager nennst oder nicht, das ist Quark. Es kommt immer mal wieder vor, dass man eine zusätzliche Instanz braucht (das kann oft in statischem Code sein), die eine gemeinsame Wahrheit für eine Gruppe von Objekten beinhaltet. Wenn diese Instanz allerdings die Kontrolle über die Objekte übernimmt bei Sachen, die sie auch eigenständig könnten, dann läuft etwas schief. Und mit dem Namen "Manager" deklariert man genau das.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Also ich für mich persönlich habe knapp 10 Jahre verschiedene Techniken mit Unity ausprobiert. Habe eigene Libraries geschrieben, die leider auch unfertig sind (kaum Zeit dafür mittlerweile). Hier muss ich auch erwähnen, dass es nur meine persönliche Erfahrung und Meinung ist. Wie man schon durch @Sascha's langen Texten sehen kann, könnte man Bücher schreiben um zu erklären, welche Techniken man verwenden kann. Das ist auch der Grund warum ich mit Youtube Kanal anfing. Nicht um zu zeigen wie man machen sollte, eher um meine eigene Erfahrung, wie ich Sachen bewältige und was ich cool finde usw.  

Ich verfolge meist eine einzige Regel in Unity. So gut wie möglich alles in seinen eigenen Script packen. Um mal einen guten Beispiel zu geben. Ich benutzte DamageSystem.cs. Dort ist Health zu finden. Allerdings was ist nun hier das Problem? Ist doof zu erweitern. Möchte ich nun Armor hinzufügen muss ich diesen Script bearbeiten. Was ist also besser? Genau, wieso nicht gleich Health.cs, Armor.cs usw. unabhängig davon schreiben.

Diese wiederum können so gut aufgebaut werden, dass bereits automatische Events usw. eingebaut werden. Beispiel UnityEvents, Automatische Veränderungschecks (quasi, ist der neue Wert wie der Alte? Nein? Dann Event auslösen usw.)
Vorteil hier ist, ich kann beliebige neue Attributen einbauen und DamageSystem muss nicht angefasst werden, in dem ich Component reinziehe oder rausziehe. Hab ich Stamina.cs Component beim Spieler, dann hat er auch Stamina. In dem Fall muss mein DamageSystem gar nicht wissen, um was es geht. Es ist wie Lieferer, der weiß nicht was in dem Paket ist, aber kann damit arbeiten.
Ebenfalls kann man auch mit GetComponent abfragen, ob Health, Armor, Stamina usw existiert und je nachdem anders handeln.

Nun wie kann ich das nun mit UI dynamisch verbinden?
Dasselbe Spiel dort auch. UI Elemente werden aktiviert oder deaktiviert je nachdem welche Componenten es existieren. Mit GetComponent oder TryGetComponent kann man abfragen, ob etwas existiert.

 

Manager

Es wurde ja bereits gesagt, dass bei Namen wie Manager Vorteile und Nachteile gibt. Um aber mal Proargumente abzugeben, kam bei mir auch ab und zu vor, dass irgendwo mal "Manager" entstanden ist, da keine eindeutige Namen zugewiesen werden konnte. Eben, weil sie gleich mehrere Aufgaben tun. Das ist manchmal auch gut so, denn ich möchte auch ein Component in Unity haben, dass es möglichst erleichtert etwas leicht zu konfigurieren. Ich bin auch kein Fan davon und benutze wirklich sehr selten sowas oder solche Namen (aber mir geht ja um Pro hier gerade).

Für mich sind meist Manager, die irgendwie eine Art Überwachung betreiben. Ein Beispiel ist, wie finde einen bestimmten Gegner auf der Karte. Wo hole ich die Information her. EnemyManager oder EnemyManagement haben solche Informationen meistens (zugeben, würde ich nicht mal so machen, aber war ein Beispiel, was mir spontan einfiel). Z.B. EnemyManager.Find(id).

Noch nie ist mir aber im Leben passiert, dass ich durch anderen 3rd Party Libraries bei einem Namen wie "Manager" aufgehalten wurde zu programmieren oder verwirrt war und ich dachte "Oh Gott, ich bin hilflos, was tut denn dieser Manager nun?" (um bissel zu übertreiben :D). Denn allein wenn man "Enemy.cs" sehen würden wissen wir ja auch nicht genau, was dort alles möglich ist. Wir wissen geht um Enemy. Was tut es denn?. Arbeitet Enemy denn mit Transform (also mit movements)? Hat es Health eingebaut? Benutzt es Animationen? Fragen über fragen. Oder sind die alle vllt. in separaten Components usw. unterteilt? Mal ja mal nein (in meinem Fall meistens, ja).

Denn bei simplen Namensvergebungen aus Kombination meist aus Nominativ und (nominalisiertesVerb also "Was ist das?" und "Was tut es?" wie zum Bespiel WorldSpawner, EnemyDamageReceiver" UIElementTransition ist man schnell informiert, aber bei komplexeren wie Util(s), DamageSystem, HUD, UI oder bei komplett erfundenen Namen wie Tama, Checky schauen wir trotzdem die Docs oder Source an um zu verstehen, was genau da möglich ist und passiert. Unabhängig davon behaupte ich mal, dass man selbst bei WorldSpawner dennoch anschauen würden, was eigentlich da alles möglich ist. Dank unser IDE sehen wir ja meist auch was möglich ist (yay!).

Natürlich, würde es WorldManager heißen, würden man auch nicht wissen, ob es eine Spawn-Funktion hat und solche aufgeben erledigt. Allerdings hab ich mal solche Klassen gesehen, die auch eine Funktion hatte. Z.B. WorldSpawner.Spawn(World world). Klasse!

Nehme als Beispiel mal Mirrors NetworkManager:
IicQKi8.png 

Das ist ein Runtime - Component. Sowohl Einstellung, aber auch sowohl Aufgaben wie Verbindungaufbauen, Callbacks ausführen etc. tut dieser Manager. Nun was könnte hier der Name am besten sein, um präzise mir verständlich zu machen, was genau die Klasse macht? Man hätte vllt auch gleich Mirror nennen können. Würde trotzdem nichts aussagen für mich. 
Aber, muss ich denn genau wissen, was im Background passiert? Denn den Usern bzw. Programmierern wollen wir doch so einfach wie möglich machen.

Ich will hier jetzt nicht als Pro Manager rüber kommen, aber ich wollte mal auch die guten Seiten mal darstellen, wieso man manchmal Manager verwendet. Ich weiß, nun man könnte ja Unterteilen. Genau dazu komm ich jetzt.

Um Mal gegen zu argumentieren.
Neben Mirror wurde aus der gleichen Projekt eine andere Version erstellt, nennt sich Mirage.
Mirage ist das komplette Gegenteil von Mirrors Vorgehensweise. Mirage fokussiert sich auf die wesentlichen Elemente des C#'s OOP. In Mirage siehst du also die ganzen Unterteilungen zu Configuration, SceneManagement usw. in verschiedenen Komponenten. Eigentlich fand ich die IDEE super, denn genau mein Geschmack... dachte ich. Am Ende hatte mein GameObject nicht nur zwei Components, wie da oben wie wir sehen "NetworkManager" und "Transport", sondern knapp ca. 10 verschiedene. Das ist für mich einfach gesagt Spaghetti. Um mal was zu ändern muss ich die Components durch gehen und bei verschiedenen Stellen etwas einstellen. Ich verliere den Übersicht und lästige hin und her gehen nervt total. Mag sein, dass Mirage hier technisch was falsch tut, aber das ist ein Beispiel, wieso mal ein "Manager" gut tut. 

Ganz großer Nachteil

Ein ganz großer Nachteil bei sowas ist, die Arbeit mit mehreren Personen. Manager sind echt nicht dafür gemacht.
Manager sind meistens abhängig bzw. die Scripte sind abhängig von Manager. Das ist genau so auch bei Singleton so. Hab bereits dafür eine gute Technik entwickelt (yay!) und alles ist automatisiert. Sollte ich also mal Singleton, oder Manager verwenden müssen, dann spawnen die auch selbstständig jedes mal, egal in welcher Scene ich starte.

 

Vorteile und Nachteile bei Fehlersuche

Ich will ehrlich sein. Ich sehe da kein Unterschied, denn beide sind aufwendig für mich.

Wenn man in seinem Manager ein Fehler hat, dann weiß man auch dieser Fehler ist irgendwo in dem Script ist. Aber wenn alles irgendwie miteinander verknüpft hat, auch wenn es decoupled arbeitet, dann hat man z.B. das Problem, dass man alles zurück verfolgen muss, wo dieser Fehler her kommt.

Beispiel A->B->D, C->D, E->D, F->D   und Der Fehler ist bei D. Nun muss ich heraus finden, wieso dieser Fehler bei D entstanden ist. Wie man durch Pfeile erkennen kann, kommen am Ende alle zu D. Das heißt A, B, C, E, F können dieser Fehler verursacht haben.

 

Was ich zum Beispiel nicht mag, aber in dem Projekt mit arbeite und leider so eine Struktur hat ist z.B. sowas:

egUFKW5.png

Uff und wieso hat er Entwickler das so gemacht???

DpMGBvO.png

 

Das tun zum Beispiel auch viele und ich verstehe nicht weshalb sie das tun 
Hier heißt der Player zwar nur Player, aber ganz klar was wir hier sehen ist, dass es ein PlayerManager ist :D.

Meine Empfehlung.. arbeite hier mit UnityEvents.

UnityEvent

UnityEvent ist Liebe <3. UnityEvents sorgt dafür, dass man zwei unabhängige Scripte zusammen arbeiten lassen kann, da du die Einstellung durch Inspector durchführst und nicht im Code. Dank Serialization kann Unity die Listener für uns registrieren.

Sagen wir mal du hast Shoot() ausgeführt und mit Raycast machst du GetComponent<Health>() und nun kannst du health.ApplyDamage(int dmg) ausführen.
Nun willst du ja UI oder Healthbar usw. updaten. UnityEvent kann das tun. Egal ob UI da ist oder nicht.. ApplyDamage wird immer funktionieren und die Scripte sind auch nicht abhängig davon.

Man stelle sich UnityEvent wie ein unsichtbarer Middlesman vor. Jemand der für uns eine Aufgabe übernimmt im Background.

Nachteil
Leider hat auch UnityEvent einen doofen Nachteil, wenn man sie ganz normal verwendet. Wie kannst du nachvollziehen, wo du UnityEvent verwendet hast.? Ist schwer. Kannst du so gut wie gar nicht.

Beispiel. Du hast ein UI Script und da steht public void UpdateHealthText(string text). Nun muss du irgendwie herausfinden, wo UpdateHealthText benutzt wird. Kannst du nicht (zumindest fällt mir da keine Idee ein, ob man das im Editor rausfinden kann). Vllt hast du es beim Spieler bei Health.cs verwendet, oder beim Player.cs oder einfach irgendwelche anderen Scripte usw. Fakt ist. Zwar wird ApplyDamage in Health funktionieren.. allerdings beim Invoke des UnityEvents wird eine Fehlermeldung rausspucken, dass UnityEvent eine Funktion nicht mehr findet. Sprich.. man muss darauf auch achten, dass man beim UnityEvent, wieder UpdateHealthText rausnimmt. Wäre eig schön, wenn UnityEvent die möglichkeit hätte sowas zu ignorieren. Dann wäre das kein Problem.

Allerdings kann man dafür eigene Visualizer schreiben, wie hier gemacht wurde: https://assetstore.unity.com/packages/tools/utilities/event-visualizer-163380 (ich will nicht lügen, habe es gerade ergooglet).

Um das umzugehen gibt es dafür gibt es ein paar Tricks. Listener und Receiver schreiben. Das ist zu deep um jetzt da reinzugehen, aber das Gute bei der Sache ist, dass der Listener immer da ist beim UI als Component und das Receiver genauso da ist beim Health. Der Listener kann quasi schauen, ob eine Instanz existiert if health == null, dann tue nix. Daher, ob du UI löschen tust, oder Health.. keine Fehlermeldung wird entstehen.

Meine eigene Lösung dazu war eine anderes eigenes System. Den nannte ich SmartEvents und muss gecodet werden. Heißt hier arbeitet man nicht mit dem Inspector. Ich finde mit IDE zu arbeiten immer bequemer, da ich auch schauen kann, wo was benutzt wird. Dafür sind die IDEs einfach zu gut geworden mittlerweile.

Wenn ihr UnityEvent liebt dann bitte downloaded unbedingt das heir: https://github.com/MerlinVR/EasyEventEditor

EasyEventEditor fügt mehr Optionen hinzu. Beispiel private Methoden werden auch sichtbar. So oder so verstehe ich nicht, wieso das allgemein nicht möglich ist.. denn manchmal will ich private Methoden nur für UnityEvent benutzen.

Wenn man richtig hardcore unterwegs sein will

Schaue dir Serialization an. Wie es funktioniert. Wenn man seine eigene Serialization schreiben kann, kann man mächtige Sachen erschaffen.

Ein Beispiel. Wir benutzen Tilemap. Wir finden aber Tilemap abzuspeichern sehr krass vom Speicherplatz her. Deswegen speichern wir die wichtigen Daten als JSON ab und lassen in Runtime da wieder lesen. Wir sparen dadurch über 80% Speicherplatz. Denn in der Scene oder als Prefab waren das für uns zu viel Daten.

Ich bin leider nicht soweit gekommen, aber quasi damit wollte ich auch meine Scripte entkoppen und mit eigenen Serialization tool arbeiten. Quasi die eigenen EventSysteme werden in JSON abgespeichert und wieder geladen. Leider konnte ich das noch nicht testen, aber die Idee könnte gut funktionieren.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Archiviert

Dieses Thema ist jetzt archiviert und für weitere Antworten gesperrt.

×
×
  • Neu erstellen...