Zwischensequenzen in Unity: Wie Du eine zusätzliche Szene startest und dann in die alte zurückspringst [Update]
Das Arbeiten mit Szenen gehört in Unity3D einfach zur Basis, an der kein Entwickler vorbei kommt. Auch wenn das Konzept sehr einfach ist, so können Szenen auch ganz schöne Kopfschmerzen bereiten, gerade wenn Du zwischen verschiedenen hin- und herwechseln möchtest..
Möchtest Du zum Beispiel von einer Hauptszene in eine Subszene springen (z.B. in eine Dialogszene oder eine Kampfszene eines RPGs), von der Du nach Vollendung wieder zurückspringen möchtest, musst Du dafür sorgen, dass alle Zustände der Hauptszene nicht verloren gehen.
Für dieses Problem gibt es eine recht einfache Lösung, die ich Euch nun vorstellen möchte.
Szenenwechsel in Unity
Seit Unity 5.3 gibt es zum Starten einer neuen Szene den Befehl LoadScene aus SceneManager-Klasse. Diese findest Du im Namespace UnityEngine.SceneManagement (bitte in den Skripten das Einbinden des Namespaces per using nicht vergessen!). Vorher wurde hierfür LoadLevel aus der Application-Klasse genommen, diese ist aber mittlerweile nicht mehr gültig.
Wenn Du nun auf herkömmliche Weise die Funktion mit einem Parameter aufrufst, z.B. SceneManager.LoadScene(1);, dann startet eine neue Szene, es wird aber die aktuelle (bisherige) Szene mit allen Objekten und Werten zerstört.
Das bedeutet, dass alle Zustände der Originalszene verloren gehen. Wenn Du die Szene später wieder startest, sind die Daten zunächst also wieder im Grundzustand. Um den alten Zustand zurück zu erhalten, müssten wir uns also irgendwas überlegen.
Daten speichern und laden
Eine Variante wäre alle notwendigen Daten, die man zum Wiederherstellen der Originalszene braucht, abzuspeichern (z.B. in einer extra Datei oder bei wenigen Daten mit PlayerPrefs). Diese müsstest Du dann beim Beenden der Szene speichern und beim Starten der Szene wieder laden und den Objekten zuweisen.
Hier ist natürlich eine Menge Programmierarbeit notwendig, da Du jeden Wert eines Objektes abspeichern und später wieder zuweisen müsstest. Und sobald Du weitere Objekte in die Hauptszene integrierst, müssen auch deren Werte abgespeichert werden. Dies bedeutet eine Menge Fleißarbeit.
DontDestroyOnLoad
Mit dem Befehle DontDestroyOnLoad kannst Du in der Awake-Methode eines Skriptes nun das Zerstören eines Objektes verhindern. Auf diese Weise könntest Du die Objekte in die neue Szene rüber retten. In der neuen Szene müsstest Du sie dann deaktivieren, damit sie dort nicht stören (sollten sie dort unerwünscht sein).
Das Problem ist nur, wenn Du dann in die alte Szene zurück wechselst, hast Du die Objekte auf einmal doppelt. Dies kann man sicher mit etwas Programmierung lösen, was allerdings nicht einfacher wird, wenn man auch noch in mehrere Subszenen springen können soll.
Additive-Überladung von LoadScene
Eine wesentlich komfortablere Möglichkeit ist die Nutzung einer Überladung von LoadScene (oder die asynchrone Variante LoadSceneAsync), die es ebenfalls seit Unity 5.3 gibt. Dabei gibst Du LoadScene noch einen zusätzlichen Enum-Parameter vom Typ LoadSceneMode mit dem Wert Additive mit. Das kann dann z.B. so aussehen: SceneManager.LoadScene(1,LoadSceneMode.Additive);.
Mit dieser Variante des Befehls kannst Du nun zur aktuellen Unity3D-Szene eine Weitere hinzuladen, sodass Du beide Szenen zu einer „zusammen-mergst“ (also zusammenfügst).
Ein großer Vorteil dieses Vorgehens ist der, dass Du keine Daten der Originalszene speichern musst, da die Szene ja immer noch existiert. Jetzt musst Du nur noch dafür sorgen, dass die Objekte der einzelnen Szenen sich nicht gegenseitig stören (ähnlich wie bei DontDestroyOnLoad).
Bitte beachte, dass Du auch hier in dem jeweiligen Skript im Kopf mit using UnityEngine.SceneManagement; den Namespace einbindest.
Und noch ein kleiner Hinweis für diejenigen, die noch ältere Projekte haben: Vor Unity 5.3 konntest Du das gleiche Ergebnis mit dem Befehl LoadLevelAdditive realisieren.
Vorgehen mit der Additive-Überladung
Wie bereits gesagt führst Du mit der Überladung zwei Szenen zu einer zusammen. Damit Du diese aber in einer Szene trotzdem noch objektmäßig trennen kannst, bedienen wir uns eines kleinen Tricks.
Wir fügen jeder dieser Szenen ein Empty GameObject zu, welches wir z.B. nach dem Namen der jeweiligen Szene benennen, z.B. „DungeonScene“, „BattleScene“, „QuestOneScene“,…. Dieses Objekt setzen wir dann mit „Reset“ auf die Initialwerte zurück (den Button dafür findest Du im Komponentenmenü der Transform-Komponente). Dies ist zwar für die Funktion nicht zwingend notwendig, hilft aber in der Praxis beim Platzieren der anderen Objekte.
Diesem Empty GameObject fügst Du nun alle Objekte Deiner Szene zu, sodass dieses schließlich das Root-Objekt aller Szenenobjekte ist. Bei besonderen Funktionalitäten muss dieses nicht zwingend gemacht werden, worauf ich gleich noch eingehe, aber grundsätzlich machen wir das erst einmal so.
Wenn Du nämlich jetzt mit der Additive-Überladung von LoadScene in einer Szene eine weitere Szene hinzu lädst, dann hast Du alle Objekte der beiden Szenen in einer, aber aufgeteilt auf die beiden Root-Objekte (also z.B. „SceneDungeon“ und „SceneBattle“). Deaktivierst Du nun über die Hierachy ein Root-Objekt, siehst Du nur die Objekte einer einzigen Szene.
Das spannende ist jetzt, dass Du über ein, zwei kleine Skripte ganz leicht zwischen den beiden Szenen wechseln kannst, ohne dass Du die erste Szene (z.B. „DungeonScene“) zerstören musst. Das vorgehen wäre dann wie folgt:
- Lade Deine Hauptszene „DungeonScene“ ganz normal z.B. mit LoadScene.
- Lade die Subszene „BattleScene“ mit der Additive-Überladung hinzu und deaktiviere das Root-Objekt von „DungeonScene“.
- Möchtest Du nun wieder in die „Dungeon Scene“ zurück, dann aktivierst Du das Root-Objekt von „DungeonScene“ und zerstörst die Szene der Zwischensequenz (in diesem Fall „BattleScene“). Dies machst Du mit dem Befehl SceneManager.UnloadScene(1);.
Ein Beispiel kannst Du Dir hier herunterladen: LoadSceneAdditiveTest.zip
Als Alternative zum letzten Schritt des Zerstörens kannst Du aber auch das Root-Objekt von „BattleScene“ einfach deaktivieren. Das eignet sich vorallem dann, wenn Du den aktuellen Zustand der Zwischensequenz nicht verlieren möchtest. Allerdings darfst Du dann natürlich beim nochmaligen hineinspringen in diese Szene diese auch nicht wieder laden sondern einfach nur das Root-Objekt aktivieren.
Sonderfälle
Es gibt einige Spezialfälle, wo es vielleicht nicht so sinnvoll ist alle GameObjects in das Root-Objekt zu packen, z.B. bei einem GameController (wie im obigen Beispielprojekt). Aber auch bei einem Inventarsystem kann es interessant sein, dass dieses mit all seinen Daten von der Hauptszene mit in die Subszene wandert.
Für so etwas könntest Du die Skripte des Inventarsystems an einem separaten GameObject „Inventory“ hängen, das sich zwar in der Hauptszene befindet, aber nicht in das Root-Objekt verschoben wird. Nur beachte, dass das Inventory-Objekt natürlich nicht zusätzlich in der Subszene existieren darf!
Lädst Du nun die Subszene und deaktivierst das Root-Objekt der Hauptszene, dann ist das Inventarsystem immer noch aktiv. In der Subszene könntest Du nun die Verbindung eines Skriptes zum „Inventory“ über ein Find oder ein FindWithTag herstellen und dann die Komponentenreferenz über ein GetComponent herstellen.
Warum Szene zerstören und nicht gleich in einer entwickeln?
Man könnte jetzt natürlich fragen, warum wir diesen Umweg über die Überladung und dem anschließenden Zerstören der Szene überhaupt machen, wenn wir sie doch vielleicht gleich wieder benötigen.
Ja, Du musst sie nicht zwangsläufig zerstören, eventuell reicht auch ein deaktivieren. Gerade wenn es um Räume geht, die bei späteren Besuchen immer noch so aussehen sollen wie nach dem ersten Verlassen, kann das Sinn machen.
Du musst auch nicht zwangsläufig die beiden Szenen (Hauptszene und Subszene) auf zwei Unity-Szenen aufteilen, Du könntest sie auch gleich von Anfang an in einer einzigen Szene entwickeln. Dann bräuchtest Du nur die jeweiligen Root-Objekte aktivieren/deaktivieren und Du musst kein Objekt mehr zerstören. Wenn es vom Spielablauf und vom Speicher her passt, dann kannst Du das machen.
Aber gerade der Speicher bzw. das Laden der ganzen Ressourcen beim Starten einer Szene leidet bei so einem Vorgehen extrem. Und wenn Du dann auch noch mehrere Subszenen hast, dann macht es aus Performancesicht selten Sinn, von Anfang an mehrere Spielszenen in einer Szene zu packen. Zudem verliert man als Spieleentwickler dann auch irgendwann den Überblick 😉
Grenzen dieses Vorgehens
Dieses Vorgehen hat in Unity3D auch seine Grenzen, das will ich natürlich auch nicht unter den Tisch fallen lassen. Wenn Du z.B. Animationen abspielst, dann werden diese durch das Deaktivieren deren Root-Objekte gestoppt und auf den Anfangszustand zurückgesetzt.
„Verlässt“ Du also Deine Hauptszene zu einer Subszene während eine Animation abgespielt wird, dann befindet sich die Animation bei der Rückkehr wieder am Anfang bzw. am Ende (sollte sie z.B. nur initial „angetriggert“ worden sein).
Fazit
Wie eingangs gesagt, kann einem das Arbeiten mit Unity3D -Szenen schon manchmal einiges an Kopfzerbrechen bereiten. Aber meistens gibt es relativ einfache Lösungen. Nur muss man für diese ab und zu etwas um die Ecke denken, wie in diesem Fall.
Fallen Dir noch andere Tipps ein, die beim Arbeiten mit Unity-Szenen hilfreich sind?
Hi Carsten,
erst einmal vielen Dank für die schnelle Reaktion auf meine Frage.
Tatsächlich ist LoadLevelAdditive genau das, was ich gesucht habe, ich hab das auch schon mehr mals in der Autovervollständigung gesehen, bin aber einfach nicht auf die Idee gekommen, dass die Methode genau das tut, was ich suche, vielleicht sollte ich einfach mal aufmerksamer die Documentation lesen.
Zu meinem Vorschlag bezüglich der „kleinen Themenreihe“: Es müssen ja keine Videos sein, genau solche Blogposts, die einfach Denkanstöße zu etwas spezielleren Problemen liefern fänd ich genau ideal. So hat man die perfekte Balance zwischen „guter Idee“ und „abstrakt genug um noch selber denken zu müssen“.
Besten Gruß,
Moe
sind diese sachen im buch enthalten ?
@Moe Vielen Dank, schön, dass ich mit diesem Artikel genau Deinen Geschmack getroffen hab 🙂 Was die Doku angeht: Unity hat da mittlerweile schon extrem viele, mächtige Funktionen an Bord. Da entdecke ich selber auch noch jedes Mal was neues, wenn ich da rein schaue.
@Rayen Nein, der Artikel selber ergänzt lediglich den Buchinhalt. Aber das angesprochene Inventarsystem wird im Buch thematisiert.
Hi Carsten,
ich habe mich jetzt ein bisschen mit LoadLevelAdditive beschäftigt und ich muss ganz klar sagen: Die Methode gehört in der nächsten Auflage deines Buchs definitiv erwähnt!
Nicht nur für das hin und her springen aus Haupt und Subszene, sondern auch für den normalen Levelübergang finde ich sie absolut genial, da wir mit ihr die Möglichkeit haben Szenen mit Parameterübergabe zu laden. Ich hab es noch nicht getestet, aber ich stelle mir grade eine Methode vor, die eine Liste von Gameobjects bekommt, die neue Szene additiv läd, alle Objekte aus der alten Szene löscht, die nicht in der Liste enthalten sind und die übrigen an das neue Szenen-RootObjekt hängt. Ich hatte noch eine ganze Reihe ungelöster Probleme mit Unity, die durch das Wissen um diese eine Methode zu einem sehr großen Teil gelöst wurden.
LoadLevelAdditive hat es viel mehr verdient im jetzigen Kapitel 4.12 vorgestellt zu werden als die PlayerPrefs, die mMn nur einen ganz, ganz kleinen Anwendungsbereich haben.
Moe
Hi Moe,
gebe Dir absolut recht, LoadLevelAdditive ist definitiv eine sehr mächtige Funktion, die ich in die nächsten Auflage mit aufnehmen sollte 🙂 Allerdings halte ich die PlayerPrefs auch für wichtig 😉
Gruß Carsten
Warum kommen keine Videos mehr Carsten?
Hallo Alex,
sorry, aber ich bin gerade arbeitsmäßig bis über beide Ohren total dicht und ich bin echt froh, wenn ich am Wochenende etwas Zeit finde und mal ein bisschen durchatmen kann 🙁
Gruß Carsten
Carsten… Freizeit wird überbewertet! 😀
@Thomas Schon klar 😉
Wurde dein Account auf YouTube gehackt?
@Severin Mir nicht bekannt. Wie kommst Du darauf?
Ich bin mir sicher es geht um ein Metal-Cover-Video, welches kurzzeitig auf deinem Kanal zu finden war. In den Kommentaren waren alle sehr erstaunt… 😀
@Miro Hihi, ja, das hat mich auch irritiert 😀 Das Video war (und ist mittlerweile wieder) auf meinem Kanal auf „nicht gelistet“ gestellt. Ich hatte nicht gedacht, dass das irgend jemanden auffällt, wenn ich das Video nach 2,5 Jahren auf „gelistet“ ändere. Aber da hat wohl YouTube mir einen Strich durch die Rechnung gemacht 😉
Oh, das ist ja mal klasse !
Ich wollte nach einer langen Pause mal wieder etwas mit Unity erstellen und diese Funktion kommt da gerade richtig ! 🙂
Danke Carsten und noch ein Frohes Neues ! 🙂
@Cacysunlee Ich wünsche Dir auch ein frohes neues Jahr! Gruß Carsten
Hi Carsten ich wollte mal einfach so eine Frage mal rein werfen leider keine möglichkeit gehabt, bzw gefunden anders zu fragen. Wenn ich mit Root Animationen arbeite und meine Kamera um 90 Grad drehen möchte wird sie immer um 91 Grad oder 89 Grad oder ähnliches gedreht wenn ich ohne den Animator arbeite klappt es ganz genau sry wenn ich jetzt hier dich mit einer Frage bombadiere!
Kleiner Hinweis, falls jemand erst jetzt über diesen Artikel stolpert:
Inzwischen sind LoadLevelAdditive und LoadLevel depricated.
Seit Unity 5.3 ist für beide Funktionalitäten SceneManager.LoadScene vorhanden (SceneManagement-namespace nicht vergessen!)
Der Großartige Vorteil: Szenen lassen sich nun über ihren Namen laden, dadurch entfällt ein umständliches Sortieren der Build-Order
Moe,
Scenes liessen sich doch schon vorher über ihren Namen laden… 😉
Aber danke für den Tipp.