Mehrspielige Strategien

Während der Projektbesprechungen als Berater für das Customer Success Team arbeite ich oft mit Kunden zusammen, die spielverändernde Anwendungen entwickeln. Diese Anwendungen haben ein Haupt- oder Themenmenü, aus dem der Spieler mehrere Spiele auswählen kann. Bei diesen Konfigurationen geht es vor allem darum, die Zeit zwischen den Spielwechseln so kurz wie möglich zu halten und eine optimale Leistung bei allen Spielen zu gewährleisten. In diesem Blog-Beitrag werden wir verschiedene Ansätze auf der Grundlage von Projektanforderungen sowie einige bewährte Verfahren untersuchen, die für jede Spielumgebung nützlich sein können, mit oder ohne Game-Switching-Setup.
Bei der Planung einer Multiapplikationsumgebung - sei es für Spiele, Unterhaltung oder industrielle Simulationen - ist die wichtigste Entscheidung, wie die ausführbaren Dateien von Spielen verwaltet werden sollen. Es gibt viele Faktoren, die diese Entscheidung beeinflussen können:
- Wie viele Spiele kann die Plattform verarbeiten?
- Wie groß sind die Spiele?
- Wurden die Spiele mit denselben Unity-Versionen erstellt? Was sind die Engpässe der Anwendung?
- Weitere Faktoren sind die Zielhardware, der Arbeitsspeicher und die CPU sowie die Festplattengeschwindigkeit (SSD vs. HDD vs. SD-Karte).
Die Beantwortung dieser Fragen und die Entscheidung darüber, wie mit ausführbaren Dateien umgegangen werden soll, ist entscheidend dafür, ob wir separate ausführbare Dateien für jedes Spiel, eine gemeinsame ausführbare Datei für mehrere Spiele oder eine Kombination aus beidem benötigen, um eine optimale Leistung der Anwendungen sicherzustellen.
Mehrere ausführbare Dateien sind eine gute Option, um Spiele zu verwalten, die mit verschiedenen Unity-Versionen erstellt wurden. Mit diesem Ansatz ist es möglich, die Zeit für den Wechsel zwischen den Spielen zu reduzieren, indem die ausführbare Datei im Speicher zwischengespeichert wird und jede Instanz im Hintergrund bleibt. Es ist jedoch nicht immer die beste Wahl, alle ausführbaren Dateien im Speicher zu halten, da dies den Speicher belasten kann. Dies sollte in Fällen vermieden werden, in denen die einzelnen Spiele einen größeren Speicherbedarf haben und/oder wenn es viele Spiele in der Spielumschaltanwendung gibt.
Um den Speicherplatz zu schonen, ist es möglich, dass sich Spiele eine einzige ausführbare Datei teilen. Die Spiele können sich in einem einzigen Unity-Projekt befinden, oder jedes kann sein eigenes Projekt haben, solange die Spiele dieselbe Unity-Version haben. Seit Unity 2022 LTS in Windows ist es möglich, mit dem Argument -datafolder einen variablen Pfad über die Kommandozeile zu übergeben ( -datafolder <Pfad_zum_Ordner> ), der den ausgewählten Spiele-Datenordner angibt, um den Wechsel zu ermöglichen. Ein potenzieller Nachteil dieses Ansatzes sind langsamere Spielwechsel. Daher ist es wichtig, dass Sie die besten Ladeverfahren befolgen, um diesen Nachteil zu verringern.
Unabhängig von der Art des Spiels, das wir entwickeln, oder von der Plattform, für die wir es entwickeln, ist es wichtig, so wenig Zeit wie möglich von der Auswahl des Spiels bis zum vollständigen Laden auf dem Bildschirm zu verbringen. Dieses Ziel ist besonders wichtig für Anwendungen zur Vermittlung von Spielen.
Eine gute Möglichkeit, das Laden zu handhaben, ist die Verwendung von Addressables. Bei Addressables werden die Inhalte nach Bedarf heruntergeladen und freigegeben. Diese Strategie des zeitversetzten Ladens ist der effizienteste Weg, um die Ladezeiten für Spiele zu verkürzen, da sie die Menge der Daten, die beim ersten Start geladen werden müssen, begrenzt. Außerdem kann es helfen, CPU-Hintergrundaktivitäten im Zusammenhang mit Hintergrundspielen zu verhindern, die zu CPU-Engpässen beitragen können. Addressables: Planung und Best Practices ist ein guter Ausgangspunkt, um mehr über Addressables zu erfahren und wie Sie damit Ihr Spiel verbessern können.
Eine gute Möglichkeit, das Laden zu beschleunigen, unabhängig davon, wie viele ausführbare Dateien wir verwenden, sind die asynchronen Lade-APIs. Beim asynchronen Laden führt der Unity-Hauptthread einen Prozess namens "main thread integration" aus, der für die Initialisierung von nativen und verwalteten Objekten in einem Zeitfenster verantwortlich ist. Da dieser Prozess einige Operationen ausführt, die nicht thread-sicher sind, wird er auf dem Haupt-Thread ausgeführt, und die Zeit, die für die Ausführung der Haupt-Thread-Integration erlaubt ist, ist begrenzt, um zu verhindern, dass das Spiel für eine lange Zeit einfriert. Die Zeit, die für die Integrationen aufgewendet werden kann, wird durch die Eigenschaft Application.backgroundLoadingPriority definiert. Wir empfehlen, die backgroundLoadingPriority während der Ladebildschirme auf High oder 50 ms zu setzen und nach Abschluss des Ladevorgangs auf BelowNormal (4 ms) oder Low (2 ms ) zurückzusetzen.
Eine weitere Möglichkeit, das Laden zu beschleunigen, ist der asynchrone Textur-Upload. Asynchrones Laden von Texturen kann die Ladezeit verringern, indem koordiniert wird, wie viel Zeit und Speicher für das Hochladen von Texturen und Meshes auf die GPU-Einstellung verwendet wird. Der Blog-Beitrag Understanding Async Upload Pipeline enthält detaillierte Informationen darüber, wie dieser Prozess funktioniert.
Diese Praktiken werden dazu beitragen, die Ladezeiten zu verkürzen:
- Minimieren Sie den Inhalt Ihrer Szene so weit wie möglich. Verwenden Sie eine Bootstrap-Szene, um nur das zu laden, was benötigt wird, damit das Spiel spielbar ist, und laden Sie dann bei Bedarf zusätzliche Szenen.
- Deaktivieren Sie die Kameras während der Ladebildschirme.
- Deaktivieren Sie UI Canvases, während sie während des Ladens aufgefüllt werden.
- Parallelisieren Sie Netzwerkanfragen.
- Vermeiden Sie komplexe Awake/Start-Implementierungen und setzen Sie Worker-Threads ein.
- Verwenden Sie immer die Texturkomprimierung.
- Streamen Sie große Mediendateien (wie Audiodateien und Texturen), anstatt sie im Speicher zu halten.
- Vermeiden Sie JSON Serializer und verwenden Sie stattdessen binäre Serializer.
Wie bereits erwähnt, ist der Arbeitsspeicher nicht das einzige Problem bei Multi-Game-Umgebungen. Auch die CPU-Hintergrundaktivität kann das Spielerlebnis beeinträchtigen. Wenn Spiele nicht aktiv gespielt werden, läuft ihre CPU weiter, was dazu führt, dass das aktive Spiel eine suboptimale Leistung erbringt, da die CPU ausgehungert wird. Eine Möglichkeit, CPU-Starvations für das aktive Spiel und alle anderen Backend-Prozesse zu verhindern, besteht darin, in den Unity-Einstellungen die Option Im Hintergrund ausführen auf false zu setzen. Im Hintergrund ausführen bewirkt, dass die Unity-Spielschleife angehalten wird, während das Spiel nicht im Fokus ist. Die Einstellung kann auch über ein Skript dynamisch geändert werden
public class ExampleClass : MonoBehaviour
{
void Example()
{
Application.runInBackground = false;
}
}
Beachten Sie, dass die Einstellung Im Hintergrund ausführen die Ausführung von benutzerdefinierten Skript-Threads nicht verhindert. Daher ist es wichtig, alle Threads von nicht spielenden Spielen über die C#-Methode Thread.Sleep in den Ruhezustand zu versetzen. Denken Sie daran, dass die Arbeit mit Hintergrund-Threads in Unity eine sorgfältige Programmierung erfordert. Da diese Threads keinen direkten Zugriff auf die API von Unity haben, ist die Wahrscheinlichkeit größer, dass es zu Problemen wie Deadlocks und Race Conditions kommt. Um dies zu verhindern, ist eine ordnungsgemäße Synchronisierung mit dem Haupt-Thread von Unity erforderlich. Um Multi-Threading richtig zu implementieren, lesen Sie den Abschnitt Einschränkungen von async- und await-Tasks auf der Handbuchseite Overview of .NET in Unity und den MSDN-Artikel über die Verwendung von Threads und Threading. Unity 6 führt die Klasse Awaitable ein, die eine bessere Unterstützung für async/await bietet.
Es kann schwierig und zeitaufwändig sein, die Ursachen von Speicherlecks zu identifizieren und zu beheben, insbesondere in den späteren Phasen der Entwicklung. So klischeehaft es auch klingen mag: Vorbeugen ist immer besser als heilen. Hier sind einige Empfehlungen, die helfen können, Lecks in jeder Spielumgebung zu vermeiden:
- Wenn Sie neue Objekte/Assets im Speicher erstellen, sollten Sie diese löschen, wenn sie nicht benötigt werden. Wenn Sie Addressables verwenden, stellen Sie sicher, dass Sie ungenutzte Assets freigeben.
- Beim Laden/Entladen von Szenen sollten die Assets ordnungsgemäß aus dem Speicher entfernt werden. Unity entlädt die Assets nicht automatisch, wenn ein Level entladen wird. Daher ist es wichtig, dass Sie sicherstellen, dass alle Zugriffe aus dem Speicher entfernt werden. Die Resources.UnloadUnusedAssets API kann beim Aufräumen von Assets helfen. Es kann jedoch CPU-Spitzen verursachen, da es ein Objekt zurückgibt, das so lange yieldet, bis der Vorgang abgeschlossen ist. Daher sollte es an nicht leistungsempfindlichen Stellen verwendet werden.
- Vermeiden Sie die häufige Verwendung von Instantiate und Destroy GameObjects. Dies kann zu unnötigen verwalteten Zuweisungen führen und ist außerdem eine kostspielige CPU-Operation. In Fällen, in denen die Verwendung von Destroy notwendig ist, sollten Sie jedoch sicherstellen, dass Sie alle Verweise auf das Objekt entfernen, um Leaked Shell Objects zu vermeiden. Wenn ein Objekt oder seine Eltern über Destroy zerstört werden, hält ein C#-Code eine Referenz auf ein Unity-Objekt und behält das verwaltete Wrapper-Objekt - seine Managed Shell - im Speicher. Sein Native Memory wird entladen, sobald die Szene, in der er sich befindet, entladen wird oder das GameObject, an das er angehängt ist, oder seine Eltern per Destroy zerstört werden. Wenn also etwas anderes, das nicht entladen wurde, immer noch auf ihn verweist, kann der verwaltete Speicher als Leaked Shell Object weiterleben.
- Seien Sie vorsichtig, wenn Sie Ereignisse mit Singletons implementieren. Singleton-Instanzen enthalten Verweise auf alle Objekte, die ihre Ereignisse abonniert haben. Wenn diese Objekte nicht so lange leben wie die Singleton-Instanz und sich nicht von diesen Ereignissen abmelden, bleiben sie im Speicher und verursachen ein Speicherleck. Wenn die Ereignisquelle vor den Zuhörern entsorgt wird, wird der Verweis gelöscht, und wenn die Zuhörer ordnungsgemäß abgemeldet werden, bleibt auch kein Verweis übrig. Um dieses Problem zu lösen und zu vermeiden, empfehlen wir, das Weak Event Pattern oder IDisposable in allen Objekten zu implementieren, die auf Singleton-Ereignisse hören, und dafür zu sorgen, dass sie in Ihrem Code ordnungsgemäß entsorgt werden. Das Weak Event Pattern ist ein Entwurfsmuster, das Ihnen bei der Verwaltung des Speichers und der Garbage Collection in der ereignisgesteuerten Programmierung hilft, insbesondere wenn es um langlebige Objekte geht. Es ist besonders nützlich, wenn Sie Abonnenten haben, die nur kurzlebig sind, der Herausgeber aber langlebig ist. Bitte beachten Sie, dass es sich hierbei um C#-spezifische Lösungen handelt, die nur mit C#-Events funktionieren und nicht direkt von UnityEvents oder dem Unity UI Toolkit unterstützt werden. Wir empfehlen daher, diese Lösungen nur in Ihren Skripten zu implementieren, die nicht von MonoBehaviour stammen.
Schließlich kann die Erstellung von Profilen, die Durchführung von CI/CD-Tests und Stresstests bereits in den frühen Entwicklungsphasen eine echte Zeitersparnis bedeuten, da die Erkennung von Lecks, sobald sie auftreten, es Ihnen ermöglicht, das Problem sofort zu beheben, Zeit bei der Fehlersuche zu sparen und eine optimale Leistung zu gewährleisten.