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?

Comments
  1. Moe
  2. rayen hajji
  3. Carsten
  4. Moe
  5. Carsten
  6. Alex.hafner00@gmail.com
  7. Carsten
  8. Thomas
  9. Carsten
  10. Severin
  11. Carsten
  12. Miro
  13. Carsten
  14. cacysunlee
  15. Carsten
  16. Philipp
  17. Moe
  18. Miro