Jump to content
Unity Insider Forum
Sign in to follow this  
Lightstorm

Kommunikation zwischen Scripts?

Recommended Posts

Hallo,

ich habe ein Script "GameManager", darüber soll der Gesamtzustand des Spiels geleitet werden. Spielzustände oder das gerade geladene Level, Schwierigkeit usw.

Und ich habe einen Script "Player", darüber werden die Zustände des Spielers bzw. Spielcharakters geleitet. In dem Script "Player" ist auch das Event OnTriggerEnter worüber ich Kollisionen erfasse.

Wenn der Spielcharakter durch eine Kollision zerstört wird will ich das nun dem GameManager irgendwie mitteilen damit der Zustand des Spiels, nämlich ein "Game Over", erfasst werden kann.

Wie kann ich das am besten machen?

 

Übrigens fällt es mir generell schwer die ideale Code Architektur zu finden. Es gibt immer mehrere Möglichkeiten und was gut und schlecht ist zeigt sich meistens im weiteren Verlauf. Wenn man später etwas neuartiges im Spiel einprogrammieren will und merkt dass es mit der bisher angefangenen Architektur der Scripts nur über eine unschöne Frickelei möglich ist. Dann ist es meist zu spät alles zu ändern.

Share this post


Link to post
Share on other sites
vor 19 Minuten schrieb Lightstorm:

Übrigens fällt es mir generell schwer die ideale Code Architektur zu finden.

Wenn du mich fragst, dann schonmal nicht mit einem Script, das "GameManager" heißt :)

Ich hab mal ein kleines Beispielprojekt für Architektur gemacht: https://gitlab.com/FlaShG/Unity-Architecture

Schau dir da mal unten die Beschreibung an, da stehen ein paar Dinge, die ich für wichtig halte bei guter Architektur. Du kannst dir das Projekt auch herunterladen, ansehen, ausprobieren und auseinandernehmen.

  • Thanks 1

Share this post


Link to post
Share on other sites

Hallo Sascha,

das sieht wirklich sehr gut aus, genau was ich gesucht und gebraucht habe. Ich schaue es mir gerade an und diese Architektur scheint wirklich sehr flexibel und sauber zu sein. Wirklich sehr interessant. Danke!

Es gibt ein zentralen Script namens "GameManager" weil ich das in dem 2d-roguelike-tutorial von Unity gesehen habe und dachte mir ok das ist wohl der richtige Ansatz.

Share this post


Link to post
Share on other sites

Ich sag immer: Wenn du ein Script namens "GameManager" hast, läuft direkt etwas falsch. Aber dennoch ist dieser Weg sehr verbreitet. Man muss immer ein bisschen aufpassen - bei Unity gibt's wie bei PHP sehr viele Tutorials von Leuten, die selber nicht so richtig Ahnung haben.

  • Like 1

Share this post


Link to post
Share on other sites

Nochmal danke für den echt wertvollen Hinweis. Auch das Thema scriptfähige Objekte war mir bisher nicht bekannt. Ich habe mir den Vortrag von Ryan Hipple und ein paar andere Videos darüber angeschaut.

Scriptable Objects, ein Event System und das modulare Prinzip für einzelne Aufgaben und Spielfunktionen. Sich damit zu beschäftigen lohnt sich denke ich und macht es einem leichter selbst komplexe Mechanismen sauber zu implementieren.

https://unity3d.com/de/how-to/architect-with-scriptable-objects

Share this post


Link to post
Share on other sites

Die XYValue-Objekte sind globale Variablen. Die sind potentiell für alle anderen Objekte einsehbar und existieren jeweils nur einmal. Oft ist das natürlich Quatsch, und man will stattdessen ganz normale Felder haben. Die XYReference-Klasse erlaubt es, Klassen zu schreiben, bei denen man pro Objekt entscheidet, was von beidem man benutzt.

Beispiel Health-Klasse. Wäre doch super, wenn man einfach nur "Health" hätte, statt "PlayerHealth" und "EnemyHealth". Ein wichtiger Unterschied dieser beiden Klassen ist, dass die tatsächliche Lebenspunkte-Zahl beim Player eher eine globale Variable sein sollte, also z.B. IntValue, bei Gegnern allerdings ergibt das keinen Sinn. Nicht nur, dass die HP jedes einzelnen Gegners nicht relevant sind, sondern auch, dass es beliebig viele Gegner geben kann, man aber nicht beliebig viele IntValues für alle machen kann.

Daher gibt man der Health-Klasse, die am Ende Spieler und Gegner benutzen, ein IntReference-Feld. Im Editor stellt man dann beim Player auf Value-Objekt und zieht das IntValue-ScriptableObject hinein, dessen Wert auch vom UI angezeigt wird, und beim Enemy-Prefab stellt man auf lokales Feld, sodass jede Instanz des Prefabs seine eigene Variable benutzt.

  • Thanks 1

Share this post


Link to post
Share on other sites

@Sascha

Danke für die Erklärung. Hat mir weiter geholfen, verstehe es nun besser.

Allerdings ahne ich schon und habe es von manchen auch gelesen dass man die Ordner mit den scriptfähigen Assets gut strukturieren sollte. Mit steigender Anzahl solcher Assets wird es zunehmend unübersichtlicher und die Dateinamen fallen oft lang und beschreiben aus, wie etwa "AsteroidRepeatInterval" oder "AsteroidDamagePerSec". Irgendwann hat man dann sehr viele solcher Dateien mit solch langen Dateinamen.

Share this post


Link to post
Share on other sites

Diese ScriptableObjects sind aber auch nicht dafür da, dass man alle seine Werte darauf auslagert. Wenn deine Asteroiden sich immer mit einer bestimmten Geschwindigkeit bewegen, dann kann man das auch problemlos als Konstante im Code definieren. Wenn der Wert je nach z.B. Level unterschiedlich sein soll (z.B. mehr Asteroiden in Level 2 als in Level 1), dann funktioniert auch ein normales lokales Feld im Spawn-Script.

Share this post


Link to post
Share on other sites

@Sascha

Hat das eigentlich ein bestimmten Grund wieso du prinzipiell alle Felder als privat definierst und über [SerializeField] im Editor zugänglich machst?

Beispielsweise in der Datei DamageTrigger.cs.

Das führt bei mir zu dem Problem dass ich innerhalb eines GameObjects auf diese privaten Variablen (damageAmount im Fall von DamageTrigger.cs) von anderen Script Komponenten nicht zugreifen kann. Deswegen habe ich das als public definiert.

Es gibt nämlich Fälle da möchte ich die Werte für damageAmount in DamageTrigger.cs oder speed in EnitityMovement.cs nicht über den Editor oder verschiedene ScriptableObjects definieren sondern über einen einzigen ScriptableObject der sämtliche Werte wie damageAmount und speed für ein Objekt Art beinhaltet.

So erstelle ich beispielsweise ein Asteroid Prefab und definiere über ein ScriptableObject EntityAsteroid  sämtliche Werte wie Sprite, Speed, DamageAmount, Direction usw. Diese Werte übergibt dann ein Script an alle jeweiligen Script Komponenten des GameObject wie EntityMovement oder DamageTrigger.

So kann ich mit einem einzigen Asteroid Prefab verschiedene Asteroids instanziieren und muss nicht 10 verschiedene Prefabs anlegen um 10 unterschiedliche Asteroiden mit gleichartigen Eigenschaften zu erstellen. Stattdessen lege ich 10 verschiedene Assets von EntityAsteroid an und definiere darin die Werte. Erst wenn es sich um ein Objekt handelt das ganz anders ist, etwa ein "SpaceshipEnemy" würde ich ein neuen Prefab erstellen.

 

Liege ich eigentlich mit der Annahme richtig das ScriptableObjects nicht nur dazu gut sind globale Variablen und Funktionen zu schaffen sondern auch um Arbeitsspeicher zu sparen? Demnach belegt ein Prefab das über 10 ScriptableObject Assets initialisiert wird weniger Arbeitsspeicher als wenn man 10 unterschiedliche Prefabs anlegt.

Share this post


Link to post
Share on other sites

Durch SerializeField wird das Feld nur für den Inspector sichtbar, um Änderungen vor zu nehmen. Ansonsten ist es für alle anderen Klassen bzw. Scripte private. Willst du also aus einem anderen Script auf dieses zugreifen muss es public sein. :)

  • Like 1

Share this post


Link to post
Share on other sites

Was @Kojote sagt. Es gibt Dinge, die du im Editor einstellen willst, weil sie das Objekt selbst betreffen - eine öffentliche Variable allerdings bedeutet Interaktion zwischen anderen Klassen und dieser, und das sind nicht immer zwei Dinge, die zusammengehören.

vor 55 Minuten schrieb Lightstorm:

Liege ich eigentlich mit der Annahme richtig das ScriptableObjects nicht nur dazu gut sind globale Variablen und Funktionen zu schaffen sondern auch um Arbeitsspeicher zu sparen? Demnach belegt ein Prefab das über 10 ScriptableObject Assets initialisiert wird weniger Arbeitsspeicher als wenn man 10 unterschiedliche Prefabs anlegt.

Das Beispiel ergibt leider keinen Sinn, aber ansonsten ist die Antwort auf die Frage "ja". Du kannst anstatt einer Komponente mit 20 Eigenschaften eine Komponente machen, die ein bestimmtes ScriptableObject referenziert und dieses hat dann 20 Eigenschaften. Wenn du dann mehrere Objekte hast, die denselben Satz Eigenschaften hat, musst du die Werte nicht in jedem Objekt noch einmal haben, sondern hast sie an einem zentralen Ort. Das verringert ein wenig RAM-Auslastung, Speicherverbrauch der Assets, und außerdem, was am wichtigsten ist: Weniger Duplikation.

Und da sind wir dann tatsächlich beim Vergleich zu Prefabs. Wenn du 10 Prefab-Instanzen in der Szene hast, ist es für den Arbeitsspeicher egal, ob sie alle von demselben Prefab instanziiert sind oder jeder von einem eigenen. Interessant ist das eher für die Duplikation in deinen Assets, dass da nicht lauter gleichartige Dinge rumschwirren, sodass du 10 Assets abändern musst, wenn du eine Änderung haben willst. Damit kommt das Thema auch ganz nah an Nested Prefabs dran, was man sich mal ansehen sollte. Ist super.

vor einer Stunde schrieb Lightstorm:

So kann ich mit einem einzigen Asteroid Prefab verschiedene Asteroids instanziieren und muss nicht 10 verschiedene Prefabs anlegen um 10 unterschiedliche Asteroiden mit gleichartigen Eigenschaften zu erstellen. Stattdessen lege ich 10 verschiedene Assets von EntityAsteroid an und definiere darin die Werte. Erst wenn es sich um ein Objekt handelt das ganz anders ist, etwa ein "SpaceshipEnemy" würde ich ein neuen Prefab erstellen.

Da lieber Prefab Variants, die sind auch Teil des neuen Prefab-Workflows.

  • Thanks 1

Share this post


Link to post
Share on other sites

@Kojote

@Sascha

Wieder mal was gelernt. Danke, das war hilfreich. Von Prefab Variants wusste ich nichts. Ich hatte mich lange mit Unity nicht beschäftigt. Seitdem kamen einige neue Features dazu.

vor einer Stunde schrieb Sascha:

Wenn du 10 Prefab-Instanzen in der Szene hast, ist es für den Arbeitsspeicher egal, ob sie alle von demselben Prefab instanziiert sind oder jeder von einem eigenen.

Wenn aber von 10 verschiedenen Prefabs Instanzen erstellt werden sollen müssen diese 10 verschiedenen Prefabs doch im Arbeitsspeicher vorliegen? Oder werden die Prefab Daten beim instanziieren von der Festplatte geladen? Soweit ich weiß liegt das aus möglichen Performance Gründen alles im Arbeitsspeicher vor. Nur Prefabs für die es keine Referenz in der aktuell geladenen Szene gibt bleiben außerhalb des Arbeitsspeicher.

Und ich erinnere mich irgendwo mal gelesen zu haben das ein Prefab um die 5MB RAM beanspruchen kann.

 

Hier ist ein Beispiel von Ryan Hipple bezüglich öffentlichen und privaten Variablen:

https://github.com/roboryantron/Unite2017/blob/master/Assets/Code/Variables/DamageDealer.cs

Er selbst legt solche Variablen als public an. Widerspricht das nicht seiner Architektur Philosophie?

Share this post


Link to post
Share on other sites
vor 20 Minuten schrieb Lightstorm:

Wenn aber von 10 verschiedenen Prefabs Instanzen erstellt werden sollen müssen diese 10 verschiedenen Prefabs doch im Arbeitsspeicher vorliegen?

Wir reden von 10 Prefabs. Wer sich da Sorgen macht, ein paar bytes abschneiden zu können, sollte mit Assembler programmieren. Das ist völlig wurscht.

vor 20 Minuten schrieb Lightstorm:

Und ich erinnere mich irgendwo mal gelesen zu haben das ein Prefab um die 5MB RAM beanspruchen kann.

Kannst ja mal ne Prefab-Datei aufmachen. Ich verspreche dir, dass ein Prefab im Arbeitsspeicher nicht nennenswert mehr Platz wegnimmt als auf der Festplatte. Prefabs können selbstverständlich beliebig groß werden, aber wer Prefabs hat, die mehrere MB groß werden, der macht sowieso irgendetwas falsch.

vor 22 Minuten schrieb Lightstorm:

Er selbst legt solche Variablen als public an. Widerspricht das nicht seiner Architektur Philosophie?

Dieses Repo ist - vorsicht, böses Wort - ziemlich hingeschissen. Viele extrem wichtige Konzepte, die man braucht, um aus der Idee ein wirklich brauchbares Framework zu machen, fehlen komplett. Und er hat ganz offensichtlich nicht darauf geachtet, alles schick zu machen. Es ist Beispielcode; und es würde mich nicht einmal wundern, wenn er da absichtlich nicht so viel Mühe reingesteckt hat. Schließlich verdient er mit der "richtigen" Version, die er gebaut hat und maintained, seinen Lebensunterhalt.

Übrigens hab ich mir auch durch @malzbie angewöhnt, öffentlichen Code für Leute, wie z.B. hier im Forum, nicht immer nach meinen Standards zu schreiben - wie er damals sagte, kann ein [SerializeField] schonmal Anfänger verwirren, was echt nicht gut ist, wenn's eigentlich gerade um etwas ganz anderes geht. So gibt es mehr Code auf der Welt, der nicht so gut ist wie er sein könnte, aber irgendwo muss man halt die Grenze ziehen :)

Share this post


Link to post
Share on other sites
vor 16 Stunden schrieb Sascha:

So gibt es mehr Code auf der Welt, der nicht so gut ist wie er sein könnte, aber irgendwo muss man halt die Grenze ziehen

Genau! :)

Trotzdem finde ich es natürlich auch wichtig die Möglichkeiten von c# und Mono zu zeigen. Kommt meiner Meinung nach immer auf den jeweiligen Stand des Fragenden an (und das erkennt man meist schon an der Frage).
Ist er weiter fortgeschritten dann ja, ist er blutiger Anfänger, dann lieber nicht, weil er sollte ersteinmal die Grundlagen verstehen.
Aber ich würde z.B. nie if(igendwas == true) schreiben. Das wäre dann doch zuviel des Guten. ;)

Share this post


Link to post
Share on other sites

Jup, so mach ich das ja auch. Ryan Hipple hatte bei dem Repo allerdings nicht den Luxus, zu wissen, wer das alles liest :)

Share this post


Link to post
Share on other sites

Im Prinzip eine hilfreiche Vorgehensweise. Aber hat auch den Nachteil dass dadurch eine Menge Code im Internet kursiert die lernende als vorbildlichen Code auffassen können. Vor allem wenn es dann von Profis kommt. Da wäre ein kurzer Kommentar das auf diese Unvollständigkeit hinweist durchaus sinnvoll. Zumindest bei umfangreicheren Beispielen und Repos.

Share this post


Link to post
Share on other sites

Mach ich tatsächlich meistens sogar - dann schreibe ich sowas wie "das kann man noch verbessern, reicht aber erstmal". Und Hipples Repo sollte man nicht anschauen, ohne vorher den Talk gesehen zu haben, und da wird auch klar, dass das nur Beispielcode ist.

Share this post


Link to post
Share on other sites

Ich glaube es ist wichtig sich immer um ein Verständnis zu bemühen wenn man etwas lernt. Lieber also eine bestimmte Vorgehensweise nachvollziehen statt einfach blind übernehmen. Deswegen habe ich auch hier gefragt wieso du die Felder privat definierst und fragte mich wieso Hipple es nicht tat. Wobei ich mir auch schon selbst dachte dass es Sinn macht die Felder möglichst privat zu definieren um die Komponenten voneinander unabhängig zu halten, war mir aber nicht sicher. Ich habe meine vorherige Vorgehensweise die öffentliche Felder erforderte daher auch unterlassen.

Wie schon gesagt tue mich mit Code Architektur schwierig. Wenn ich dann eine Architektur anfange zu lernen entstehen beim programmieren Problemlösungen wo ich mir aber gar nicht mehr sicher bin ob ich eigentlich noch im Rahmen der ausgewählten Architektur agiere und man die eigentlichen Grundideen eigentlich richtig verstanden hat. Das braucht wohl einfach etwas Erfahrung.

Share this post


Link to post
Share on other sites

@Sascha

Kannst du bitte etwas zu dem Monitor Code sagen? Was tut das genau und wie nutzt man es?

https://gitlab.com/FlaShG/Unity-Architecture/blob/master/Assets/Plugins/MiniHipple/Values/ValueObject.cs

https://gitlab.com/FlaShG/Unity-Architecture/blob/master/Assets/Plugins/MiniHipple/Values/References/ValueReference.cs

Soweit ich verstehe werden Wertänderungen an die angemeldeten Objekte gemeldet. Aber so ganz verstehe ich es nicht. Wodurch sollen die Methoden AddMonitor und RemoveMonitor aufgerufen werden?

Share this post


Link to post
Share on other sites
vor 31 Minuten schrieb Lightstorm:

Soweit ich verstehe werden Wertänderungen an die angemeldeten Objekte gemeldet.

Korrekt.

vor 32 Minuten schrieb Lightstorm:

Aber so ganz verstehe ich es nicht. Wodurch sollen die Methoden AddMonitor und RemoveMonitor aufgerufen werden?

Diese Methoden werden von den Objekten aufgerufen, die sich für Änderungen des Wertes interessieren. Ein Beispiel hast du hier: https://gitlab.com/FlaShG/Unity-Architecture/blob/master/Assets/Scripts/IntValueUIDisplay.cs

Das ist die Klasse, die mithilfe einer Text-Komponente den Wert einer IntValue anzeigt. Wann immer sich der Wert ändert, wird der neue Wert angezeigt, anstatt z.B. jeden Frame in Update nachzuschauen, ohne dass sich etwas geändert hätte.

Das Ganze ist ein handelsübliches Observer Pattern (Beobachtermuster). Stelle es dir wie einen Newsletter vor, den Leute abonnieren können. Wann immer es eine Neuigkeit gibt, geht der Mailverteiler durch seine Liste der Empfänger und schickt jedem eine E-Mail. AddMonitor und RemoveMonitor sind quasi das Abonnieren und das Abbestellen des Newsletters.

  • Thanks 1

Share this post


Link to post
Share on other sites

@Sascha

Danke, habe es nun verstanden und konnte es erfolgreich umsetzen. Es mag ein übliches Pattern sein, aber ich finde es genial :D

Vorher habe ich die Veränderung des Score Wertes im Spiel für die Anzeige als Text über ein GameEvent realisiert.

  • Like 1

Share this post


Link to post
Share on other sites
vor 5 Minuten schrieb Lightstorm:

Es mag ein übliches Pattern sein, aber ich finde es genial :D

Der Hinweis war vor allem dafür, dass du weißt, dass du im Zweifelsfall ienige allgemeine Informationen für das Muster finden kannst, ohne dich bei der Recherche auf Unity zu beschränken ;)

Freut mich aber auf jeden Fall, dass es läuft!

Share this post


Link to post
Share on other sites

@Sascha

Wieder mal eine Frage :D

Eines verstehe ich noch nicht. Wenn man ein Feld wie "[SerializeField] private int test" anlegt kann man es beispielsweise in der Reset Methode mit "test = 10" initialisieren. Aber wenn es ein Feld wie "[SerializeField] private IntReference test" ist und man es in der Reset Methode mit "test.Value = 10" festlegen will gibt es einen Fehler:

NullReferenceException: Object reference not set to an instance of an object

Um das zu beheben muss man das Objekt direkt erstellen. Also "[SerializeField] private IntReference test = new IntReference()".

Wieso ist das so? Wenn man auf die Referenz Variable in irgendeiner anderen Methode im Script zugreift klappt das ohne zuvor "new IntReference()" aufzurufen. Den NullReferenceException gibt es meistens wenn man auf das Feld in einer frühen Phase zugreifen will. Also in der Reset Methode im Editor oder manchmal auch in der Start Methode.

Share this post


Link to post
Share on other sites

Der default-Wert von IntReference ist null, also "keins". Mit einem Objekt, das nicht existiert, kann man halt nicht arbeiten. Darum schreibst du in Reset auch "test = new IntReference(10);".

  • Thanks 1

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×