Was Sie auf dieser Seite finden: Tipps von Michelle Martin, Software-Ingenieurin bei MetalPop Games, zur Optimierung von Spielen für verschiedene mobile Geräte, damit Sie so viele potenzielle Spieler wie möglich erreichen können.
Mit ihrem mobilen Strategiespiel Galactic Colonies standen MetalPop Games vor der Herausforderung, es den Spielern zu ermöglichen, riesige Städte auf ihren Low-End-Geräten zu bauen, ohne dass die Bildrate einbricht oder das Gerät überhitzt. Sehen Sie, wie sie ein Gleichgewicht zwischen guter Optik und solider Leistung gefunden haben.
So leistungsfähig mobile Geräte heute auch sind, es ist immer noch schwierig, große und gut aussehende Spielumgebungen mit einer soliden Bildrate auszuführen. Solide 60 Bilder pro Sekunde in einer großen 3D-Umgebung auf einem älteren Mobilgerät zu erreichen, kann eine Herausforderung sein.
Als Entwickler könnten wir einfach High-End-Handys ins Visier nehmen und davon ausgehen, dass die meisten Spieler über ausreichende Hardware verfügen, um unser Spiel reibungslos zu spielen. Dies wird jedoch dazu führen, dass eine große Anzahl potenzieller Spieler ausgeschlossen wird, da noch viele ältere Geräte in Gebrauch sind. Das sind alles potenzielle Kunden, die Sie nicht ausschließen wollen.
In unserem Spiel Galactic Colonies kolonisieren die Spieler fremde Planeten und errichten riesige Kolonien, die aus einer großen Anzahl von Einzelgebäuden bestehen. Während kleinere Kolonien vielleicht nur ein Dutzend Gebäude haben, können größere Kolonien leicht Hunderte von Gebäuden haben.
So sah unsere Zielliste aus, als wir mit dem Aufbau unserer Pipeline begonnen haben:
- Wir wollen riesige Maps mit einer großen Zahl von Gebäuden.
- Wir wollen schnell auf billigeren und/oder älteren mobilen Geräten laufen
- Die Beleuchtung und Schatten sollen gut aussehen.
- Wir wollen eine unkomplizierte und wartbare Produktionspipeline.
Eine gute Beleuchtung in Ihrem Spiel ist der Schlüssel dazu, dass die 3D-Modelle gut aussehen. In Unity ist das ganz einfach: Richten Sie Ihren Level ein, platzieren Sie Ihre dynamischen Lichter und schon können Sie loslegen. Und wenn Sie die Leistung im Auge behalten müssen, backen Sie einfach alle Lichter und fügen Sie über den Post-Processing-Stack etwas SSAO und andere Augenweiden hinzu. Bitte sehr, versenden Sie es!
Für mobile Spiele braucht man eine ganze Reihe von Tricks und Lösungen, um die Beleuchtung einzurichten. Wenn Sie nicht auf High-End-Geräte abzielen, sollten Sie zum Beispiel keine Nachbearbeitungseffekte verwenden. Ebenso wird eine große Szene voller dynamischer Lichter Ihre Framerate drastisch senken.
Echtzeit-Beleuchtung kann auf einem Desktop-PC sehr teuer sein. Auf mobilen Geräten sind die Ressourcen noch stärker begrenzt und man kann sich nicht immer all die schönen Funktionen leisten, die man gerne hätte.
Sie wollen also nicht, dass die Akkus Ihrer Nutzer durch zu viele Lichter in Ihrer Szene mehr als nötig entladen werden.
Wenn Sie ständig an die Grenzen der Hardware stoßen, wird das Telefon heiß und drosselt folglich die Leistung, um sich selbst zu schützen. Um dies zu vermeiden, können Sie jedes Licht, das keine Schatten in Echtzeit wirft, backen.
Beim Light-Baking werden die Lichter und Schatten einer (statischen) Szene vorberechnet und in einer Lightmap gespeichert. Der Renderer weiß dann, wo er ein Modell heller oder dunkler machen muss, um die Illusion von Licht zu erzeugen.
Das Rendern von Dingen auf diese Weise ist schnell, weil alle teuren und langsamen Lichtberechnungen offline durchgeführt wurden und der Renderer (Shader) zur Laufzeit nur noch das Ergebnis in einer Textur nachsehen muss.
Der Nachteil dabei ist, dass Sie einige zusätzliche Lightmap-Texturen mitliefern müssen, was die Größe Ihres Builds erhöht und zusätzlichen Texturspeicher zur Laufzeit erfordert. Sie werden auch etwas Platz verlieren, weil Ihre Meshes Lightmap-UVs benötigen und ein bisschen größer werden. Aber insgesamt werden Sie einen enormen Geschwindigkeitsschub erhalten.
Für unser Spiel war dies jedoch keine Option, da die Spielwelt in Echtzeit vom Spieler aufgebaut wird. Die Tatsache, dass ständig neue Regionen entdeckt, neue Gebäude hinzugefügt oder bestehende ausgebaut werden, verhindert jede Art von effizientem Lichtbacken. Es reicht nicht aus, einfach auf den Back-Knopf zu drücken, wenn man eine dynamische Welt hat, die vom Spieler ständig verändert werden kann.
Daher sahen wir uns mit einer Reihe von Problemen konfrontiert, die beim Backen von Lichtern für hochmodulare Szenen auftreten.
Light-Baking-Daten werden in Unity gespeichert und direkt mit den Szenendaten verknüpft. Dies ist kein Problem, wenn Sie einzelne Ebenen und vorgefertigte Szenen und nur eine Handvoll dynamischer Objekte haben. Sie können die Beleuchtung vorbacken und sind fertig.
Das funktioniert natürlich nicht, wenn Sie die Ebenen dynamisch erstellen. In einem Städtebauspiel wird die Welt nicht im Voraus erstellt. Stattdessen wird es weitgehend dynamisch und on-the-fly durch die Entscheidung des Spielers zusammengestellt, was und wo gebaut werden soll. Dies geschieht in der Regel durch die Instanziierung von Prefabs, wenn der Spieler etwas bauen möchte.
Die einzige Lösung für dieses Problem besteht darin, alle relevanten Daten zum Lichtbacken im Prefab statt in der Szene zu speichern. Leider gibt es keine einfache Möglichkeit, die Daten für die zu verwendende Lightmap, ihre Koordinaten und den Maßstab in eine Prefab zu kopieren.
Der beste Ansatz für eine solide Pipeline, die mit leicht gebackenen Prefabs umgehen kann, besteht darin, die Prefabs in einer anderen, separaten Szene (eigentlich in mehreren Szenen) zu erstellen und sie dann bei Bedarf in das Hauptspiel zu laden. Jedes modulare Teil ist leicht gebacken und wird dann bei Bedarf in das Spiel geladen.
Wenn du dir genau ansiehst, wie Light Baking in Unity funktioniert, wirst du sehen, dass das Rendern eines Light Baked Meshes eigentlich nur das Anwenden einer anderen Textur darauf ist und das Aufhellen, Abdunkeln (oder manchmal auch Einfärben) des Meshes ein bisschen. Alles, was Sie brauchen, ist die Lightmap-Textur und die UV-Koordinaten, die beide von Unity während des Light-Baking-Prozesses erstellt werden.
Während des Light-Backing-Prozesses erstellt Unity einen neuen Satz von UV-Koordinaten (die auf die Lightmap-Textur zeigen) sowie einen Offset und eine Skalierung für das einzelne Mesh. Das erneute Backen von Lichtern ändert diese Koordinaten jedes Mal.
Um eine Lösung für dieses Problem zu entwickeln, ist es hilfreich zu verstehen, wie UV-Kanäle funktionieren und wie sie am besten genutzt werden können.
Jedes Netz kann mehrere Sätze von UV-Koordinaten haben (in Unity UV-Kanäle genannt). In den meisten Fällen reicht ein Satz UVs aus, da die verschiedenen Texturen (Diffuse, Spec, Bump usw.) die Informationen alle an der gleichen Stelle im Bild speichern.
Aber wenn Objekte eine Textur gemeinsam nutzen, wie z. B. eine Lightmap, und die Informationen einer bestimmten Stelle in einer großen Textur nachschlagen müssen, führt oft kein Weg daran vorbei, einen weiteren Satz von UVs hinzuzufügen, der mit dieser gemeinsamen Textur verwendet werden kann.
Der Nachteil mehrerer UV-Koordinaten ist, dass sie zusätzlichen Speicher verbrauchen. Wenn Sie zwei Sätze von UVs anstelle von einem verwenden, verdoppeln Sie die Anzahl der UV-Koordinaten für jeden einzelnen Scheitelpunkt des Netzes. Jeder Scheitelpunkt speichert nun zwei Gleitkommazahlen, die beim Rendern auf die GPU hochgeladen werden.
Unity generiert die Koordinaten und die Lightmap, indem es die reguläre Light-Baking-Funktionalität verwendet. Die Engine wird die UV-Koordinaten für die Lightmap in den zweiten UV-Kanal des Modells schreiben. Es ist wichtig zu beachten, dass der primäre Satz von UV-Koordinaten hierfür nicht verwendet werden kann, da das Modell ausgepackt werden muss.
Stellen Sie sich einen Kasten vor, bei dem jede Seite die gleiche Textur hat: Die einzelnen Seiten des Kastens haben alle die gleichen UV-Koordinaten, da sie die gleiche Textur verwenden. Dies funktioniert jedoch nicht bei einem mit Licht gezeichneten Objekt, da jede Seite des Kastens einzeln von Licht und Schatten getroffen wird. Jede Seite benötigt einen eigenen Bereich in der Lichtkarte mit ihren individuellen Beleuchtungsdaten. Daher ist ein neuer Satz von UVs erforderlich.
Um eine neue lichtgebackene Prefab zu erstellen, müssen wir nur die Textur und ihre Koordinaten speichern, damit sie nicht verloren gehen, und sie in die Prefab kopieren.
Nachdem das Lichtbacken abgeschlossen ist, führen wir ein Skript aus, das alle Meshes in der Szene durchläuft und die UV-Koordinaten in den aktuellen UV2-Kanal des Meshes schreibt, wobei die Werte für Offset und Skalierung angewendet werden.
Der Code zum Ändern der Maschen ist relativ einfach (siehe Beispiel unten).
Um genauer zu sein: Dies geschieht mit einer Kopie der Meshes und nicht mit dem Original, da wir während des Backvorgangs weitere Optimierungen an unseren Meshes vornehmen werden.
Die Kopien werden automatisch erstellt, in einer Prefab gespeichert und mit einem neuen Material mit einem eigenen Shader und der neu erstellten Lightmap versehen. Dadurch bleiben unsere Originalmeshes unangetastet, und die leicht gebackenen Prefabs sind sofort einsatzbereit.
Das macht den Arbeitsablauf sehr einfach. Um den Stil und das Aussehen der Grafiken zu aktualisieren, öffnen Sie einfach die entsprechende Szene, nehmen Sie alle Änderungen vor, bis Sie zufrieden sind, und starten Sie dann den automatischen Bake-and-Copy-Prozess. Wenn dieser Prozess abgeschlossen ist, wird das Spiel die aktualisierten Prefabs und Meshes mit der aktualisierten Beleuchtung verwenden.
Die eigentliche Lightmap-Textur wird von einem benutzerdefinierten Shader hinzugefügt, der die Lightmap beim Rendern als zweite Lichttextur auf das Modell anwendet. Der Shader ist sehr einfach und kurz, und abgesehen von der Anwendung der Farbe und der Lightmap, berechnet er einen billigen, gefälschten Spiegel-/Glanzeffekt.
Hier ist der Shader-Code; das Bild oben zeigt ein Material-Setup, das diesen Shader verwendet.
In unserem Fall haben wir vier verschiedene Szenen mit allen Prefabs eingerichtet. In unserem Spiel gibt es verschiedene Biome wie die Tropen, das Eis, die Wüste usw., und wir haben unsere Szenen entsprechend aufgeteilt.
Alle Prefabs, die in einer bestimmten Szene verwendet werden, teilen sich eine einzige Lightmap. Das bedeutet eine einzige zusätzliche Textur, zusätzlich zu den Prefabs, die sich nur ein Material teilen. Dadurch konnten wir alle Modelle statisch rendern und fast unsere gesamte Welt in nur einem Zeichenaufruf rendern.
Die Licht-Back-Szenen, in denen alle unsere Kacheln/Gebäude aufgebaut sind, verfügen über zusätzliche Lichtquellen, um lokalisierte Highlights zu erzeugen. Sie können so viele Lichter in den Setup-Szenen platzieren, wie Sie benötigen, da sie ohnehin alle gebacken werden.
Der Backvorgang wird in einem benutzerdefinierten UI-Dialog abgewickelt, der alle notwendigen Schritte übernimmt. Sie gewährleistet dies:
- Richtige Materialzuordnung für alle Meshes
- Alles, was während des Prozesses nicht gebacken werden muss, wird ausgeblendet
- Die Maschen werden kombiniert/gebacken
- Die UVs werden kopiert und die Prefabs erstellt
- Alles wird richtig benannt und die notwendigen Dateien aus dem Versionskontrollsystem ausgecheckt.
Ordnungsgemäß benannte Prefabs werden aus den Meshes erstellt, so dass der Spielcode sie direkt laden und verwenden kann. Die Metadateien werden bei diesem Vorgang ebenfalls geändert, damit die Verweise auf die Meshes der Prefabs nicht verloren gehen.
Mit diesem Arbeitsablauf können wir unsere Gebäude nach Belieben verändern, sie so beleuchten, wie wir es wollen, und dann alles dem Skript überlassen.
Wenn wir zurück zu unserer Hauptszene wechseln und das Spiel starten, funktioniert es einfach - ohne manuelle Eingriffe oder andere Aktualisierungen.
Einer der offensichtlichen Nachteile einer Szene, in der die Beleuchtung zu 100 % vorgebacken ist, besteht darin, dass es schwierig ist, dynamische Objekte oder Bewegungen zu erzeugen. Alles, was einen Schatten wirft, würde eine Licht- und Schattenberechnung in Echtzeit erfordern, was wir natürlich gerne vermeiden würden.
Aber ohne bewegliche Objekte würde die 3D-Umgebung statisch und tot wirken.
Wir waren natürlich bereit, mit einigen Einschränkungen zu leben, da unsere oberste Priorität darin bestand, eine gute Grafik und ein schnelles Rendering zu erreichen. Um den Eindruck einer lebendigen, sich bewegenden Weltraumkolonie oder Stadt zu erwecken, brauchte man nicht viele Objekte, die sich tatsächlich bewegten. Und für die meisten davon waren nicht unbedingt Schatten erforderlich, oder zumindest würde das Fehlen der Schatten nicht auffallen.
Wir begannen damit, alle Stadtbausteine in zwei separate Prefabs aufzuteilen. Einen statischen Teil, der die meisten Scheitelpunkte, also alle komplexen Teile unseres Netzes, enthält, und einen dynamischen Teil, der so wenige Scheitelpunkte wie möglich enthält.
Die dynamischen Teile eines Prefab sind animierte Bits, die über die statischen Teile gelegt werden. Sie sind nicht beleuchtet, und wir haben einen sehr schnellen und billigen Shader für unechte Beleuchtung verwendet, um die Illusion zu erzeugen, dass das Objekt dynamisch beleuchtet ist.
Die Objekte haben auch entweder keinen Schatten oder wir haben einen falschen Schatten als Teil des dynamischen Teils erstellt. Die meisten unserer Flächen sind flach, so dass dies in unserem Fall kein großes Hindernis darstellte.
Es gibt keine Schatten auf den dynamischen Teilen, aber das fällt kaum auf, es sei denn, man weiß, wonach man suchen muss. Die Beleuchtung der dynamischen Prefabs ist ebenfalls eine Fälschung - es gibt überhaupt keine Echtzeitbeleuchtung.
Die erste billige Abkürzung, die wir nahmen, war, die Position unserer Lichtquelle (Sonne) in den Shader für die unechte Beleuchtung zu programmieren. Es ist eine Variable weniger, die der Shader nachschlagen und dynamisch aus der Welt füllen muss.
Es ist immer schneller, mit einer Konstante zu arbeiten als mit einem dynamischen Wert. Damit haben wir eine Grundbeleuchtung, helle und dunkle Seiten der Meshes.
Um die Dinge etwas glänzender zu machen, haben wir den Shadern sowohl für die dynamischen als auch für die statischen Objekte eine unechte Spiegelung/Glanzberechnung hinzugefügt. Spiegelnde Reflexionen tragen dazu bei, ein metallisches Aussehen zu erzeugen, vermitteln aber auch die Krümmung einer Oberfläche.
Da es sich bei Glanzlichtern um eine Form der Reflexion handelt, ist der Winkel der Kamera und der Lichtquelle zueinander erforderlich, um sie korrekt zu berechnen. Wenn die Kamera bewegt oder gedreht wird, ändert sich der Glanzpunkt. Für jede Shader-Berechnung ist der Zugriff auf die Kameraposition und jede Lichtquelle in der Szene erforderlich.
In unserem Spiel haben wir jedoch nur eine Lichtquelle, die wir für die Spiegelung verwenden: die Sonne. In unserem Fall bewegt sich die Sonne nie und kann als gerichtetes Licht betrachtet werden. Wir können den Shader stark vereinfachen, indem wir nur ein einziges Licht verwenden und eine feste Position und einen festen Einfallswinkel für dieses Licht voraussetzen.
Noch besser ist, dass unsere Kamera in Galactic Colonies die Szene aus der Draufsicht zeigt, wie bei den meisten Stadtaufbauspielen. Die Kamera kann ein wenig geneigt und gezoomt werden, aber nicht um die Hochachse gedreht werden.
Insgesamt betrachtet sie die Umwelt immer von oben. Um einen billigen spiegelnden Look vorzutäuschen, taten wir so, als wäre die Kamera völlig unbeweglich, und der Winkel zwischen Kamera und Licht war immer gleich.
Auf diese Weise könnten wir wieder einen konstanten Wert in den Shader einprogrammieren und auf diese Weise einen billigen Glanz-Effekt erzielen.
Die Verwendung eines festen Winkels für die Spiegelung ist natürlich technisch nicht korrekt, aber es ist praktisch unmöglich, den Unterschied wirklich zu erkennen, solange sich der Kamerawinkel nicht stark ändert.
Für den Spieler sieht die Szene immer noch korrekt aus, was ja der Sinn von Echtzeit-Beleuchtung ist.
Bei der Beleuchtung einer Umgebung in einem Echtzeit-Videospiel ging und geht es darum, visuell korrekt zu erscheinen und nicht darum, physikalisch korrekt simuliert zu werden.
Da fast alle unsere Meshes ein gemeinsames Material haben und viele Details von der Lightmap und den Vertices stammen, haben wir eine Specular-Texture-Map hinzugefügt, um dem Shader mitzuteilen, wann und wo er den Spec-Wert anwenden soll und wie stark. Der Zugriff auf die Textur erfolgt über den primären UV-Kanal, so dass kein zusätzlicher Koordinatensatz erforderlich ist. Und weil es nicht viele Details enthält, ist es sehr niedrig aufgelöst und nimmt kaum Platz ein.
Für einige unserer kleineren dynamischen Bits mit einer geringen Anzahl von Vertexen könnten wir sogar Unitys automatisches dynamisches Batching nutzen, um das Rendering weiter zu beschleunigen.
All diese gebackenen Schatten können manchmal zu neuen Problemen führen, insbesondere wenn man mit relativ modularen Gebäuden arbeitet. In einem Fall hatten wir ein Lagerhaus, das der Spieler bauen konnte und das die Art der darin gelagerten Waren auf dem Gebäude selbst anzeigte.
Dies führt zu Problemen, da wir ein helles Objekt auf einem hellen Objekt haben, das ebenfalls hell ist. Lightbake-Empfang!
Wir haben uns dem Problem mit einem weiteren billigen Trick genähert:
- Die Fläche, auf dem das zusätzliche Objekt hinzugefügt werden sollte, musste plan sein und einen spezifischen Grauton verwenden, der dem Basisgebäude entspricht.
- Durch diesen Kompromiss war das Baking der Objekte auf einer kleineren planen Fläche möglich und wir konnten diese mit einem geringfügigen Versatz auf dem Bereich platzieren.
- Lichter, Highlights, farbige Glanzeffekte und Schatten wurden alle in die Kachel eingebrannt.
Das Bauen und Backen unserer Prefabs auf diese Weise ermöglicht es uns, riesige Karten mit Hunderten von Gebäuden zu erstellen und gleichzeitig die Anzahl der Draw Calls sehr gering zu halten. Unsere gesamte Spielwelt wird mehr oder weniger mit nur einem Material gerendert und wir sind an einem Punkt angelangt, an dem die Benutzeroberfläche mehr Zeichenaufrufe verbraucht als unsere Spielwelt. Je weniger verschiedene Materialien Unity rendern muss, desto besser ist es für die Leistung Ihres Spiels.
Das lässt uns reichlich Spielraum, um unserer Welt weitere Dinge hinzuzufügen, z. B. Partikel, Wettereffekte und andere Elemente, die das Auge erfreuen.
Auf diese Weise können auch Spieler mit älteren Geräten große Städte mit Hunderten von Gebäuden bauen und dabei stabile 60fps beibehalten.