Großstadt für mobile Geräte auf Unity. Erfahrung in Entwicklung und Optimierung



Hallo Habr! In dieser Veröffentlichung möchte ich die Erfahrung der Entwicklung eines riesigen Handyspiels mit einer großen Stadt und Verkehr teilen. Die in der Veröffentlichung beschriebenen Beispiele und Techniken erheben keinen Anspruch darauf, als Referenz und Ideal bezeichnet zu werden. Ich bin kein zertifizierter Spezialist und möchte meine Erfahrungen nicht wiederholen. Das Ziel des Spiels war es, interessante Erfahrungen zu sammeln und ein optimiertes Spiel mit einer offenen Welt zu erhalten. Während der Entwicklung habe ich versucht, den Code so weit wie möglich zu vereinfachen. Leider habe ich kein ECS benutzt, sondern mit Singleton gesündigt.

Das Spiel


Ein Spiel zum Thema Mafia. Im Spiel habe ich versucht, Amerika 30-40 neu zu erstellen. Ein Spiel ist im Wesentlichen eine wirtschaftliche Strategie aus der ersten Person. Der Spieler erfasst das Geschäft und versucht, es über Wasser zu halten.
Implementiert: Autoverkehr (Ampeln, Kollisionsvermeidung), menschlicher Verkehr, Bar, Casino, Club, Spielerwohnung, Kauf eines Anzugs, Anzugwechsel, Kauf / Lackieren / Tanken, Polizei, Sicherheit / Gangster, Wirtschaftlichkeit, Verkauf / Kauf von Ressourcen.

Architektur


Bild

Ich bedauere, dass ich kein ECS verwendet habe, sondern versucht habe, Fahrrad zu fahren. Am Ende stellte sich heraus, dass alles umständlich und zu abhängig war. Die Anwendung hat einen Einstiegspunkt - das Anwendungsobjekt (go), an dem die gleichnamige Anwendungsklasse hängt. Er ist verantwortlich für das Vorladen der Datenbank, das Auffüllen von Pools und die Grundeinstellungen. Darüber hinaus fallen mehrere andere Singleton-Manager-Komponentenklassen auf die Schultern der Anwendung (go).

  • Audiomanager
  • UIManager
  • Inputmanager

Ich habe fanatisch versucht, eine solche Architektur zu erstellen, in der ich verschiedene Komponenten vom Manager aus verwalten kann. Beispielsweise verwaltet AudioManager alle Sounds. UIManager enthält alle Elemente und Methoden der Benutzeroberfläche für die Verwaltung. Alle Eingaben werden über den InputManager mithilfe von Ereignissen und Delegaten verarbeitet.

Vereinfachter AudioManager. Sie können dem Spielobjekt so viele Audiokomponenten hinzufügen und bei Bedarf Sound abspielen:

public class AudioManager : MonoBehaviour { public static AudioManager instance = null; //  public AudioClip metalHitAC; //   private AudioSource metalHitAS; //    public bool isMetalHit = false; private void Awake() { if (instance == null) instance = this; else if (instance == this) Destroy(gameObject); } void Start() { metalHitAS = AddAudio(metalHitAC, false, false, 0.3f, 1); } void LateUpdate() { if (isMetalHit) { metalHitAS.Play(); isMetalHit = false; } } AudioSource AddAudio(AudioClip clip, bool loop, bool playAwake, float vol, float pitch) { var newAudio = gameObject.AddComponent<AudioSource>(); newAudio.clip = clip; newAudio.loop = loop; newAudio.playOnAwake = playAwake; newAudio.volume = vol; newAudio.pitch = pitch; newAudio.minDistance = 10; return newAudio; } public AudioSource AddAudioToGameObject(AudioClip clip, bool loop, bool playAwake, float vol, float pitch, float minDistance, float maxDistance, GameObject go) { var newAudio = go.AddComponent<AudioSource>(); newAudio.spatialBlend = 1; newAudio.clip = clip; newAudio.loop = loop; newAudio.playOnAwake = playAwake; newAudio.volume = vol; newAudio.pitch = pitch; newAudio.minDistance = minDistance; newAudio.maxDistance = maxDistance; return newAudio; } } 

Beim Start fügt die AddAudio-Methode eine Komponente hinzu, und dann können wir von überall den gewünschten Sound abspielen:

 AudioManager.instance.isMetalHit = true; 

In diesem Beispiel wäre es klüger, die Wiedergabe des Oneshots in die Methode zu integrieren.

Wie ein vereinfachter InputManager aussieht:

 public class InputManager : MonoBehaviour { public static InputManager instance = null; public float horizontal, vertical; public delegate void ClickAction(); public static event ClickAction OnAimKeyClicked; //public delegate void ClickActionFloatArg(float arg); //public static event ClickActionFloatArg OnRSliderValueChange, OnGSliderValueChange, OnBSliderValueChange; public void AimKeyDown() { OnAimKeyClicked(); } } 

Ich habe die AimKeyDown- Methode auf die Schaltfläche gesetzt und das Waffensteuerungsskript auf OnAimKeyClicked signiert:

 InputManager.instance.OnAimKeyClicked += GunShot; 

Mein gesamtes Eingabesystem ist auf ähnliche Weise implementiert. Ich habe keine Probleme mit der Geschwindigkeit bemerkt. Dadurch konnten wir alle Klick-Handler an einem Ort sammeln - dem InputManager.

Optimierung


Kommen wir zum interessantesten. Für Anfänger ist das Thema Optimierung in Unity schmerzhaft und mit vielen Fallstricken behaftet. Ich werde teilen, womit ich es zu tun hatte.

1. Komponenten-Caching (beginnen Sie mit einfachen Grundlagen)

In Toster können Sie häufig auf Fragen mit Beispielen stoßen, wenn GetComponent in Update verwendet wird. Dies ist nicht möglich. GetComponent sucht nach einer Komponente für das Objekt. Dieser Vorgang ist langsam und führt bei Update dazu, dass Sie wertvolle FPS verlieren. Hier finden Sie eine gute Erklärung für das Zwischenspeichern von Komponenten .

2. Verwenden von SendMessage

Die Verwendung von SendMessage () ist langsamer als die Verwendung von GetComponent (). SendMessage durchläuft jedes Skript, um mithilfe des Zeichenfolgenvergleichs die Methode mit dem gewünschten Namen zu finden. GetComponent findet das Skript durch Typvergleich und ruft die Methode direkt auf.

3. Vergleich von Objekt-Tags

Verwenden Sie die CompareTag-Methode anstelle von obj.tag == "string". In Unity wird durch Extrahieren von Zeichenfolgen aus Spielobjekten eine doppelte Zeichenfolge erstellt, die dem Garbage Collector zusätzliche Arbeit hinzufügt. Es ist besser zu vermeiden, den Namen des Spielobjekts zu erhalten. Sie können CompareTag in Update nicht aufrufen und keine schweren Vorgänge lesen.

4. Materialien

Je weniger Materialien desto besser. Reduzieren Sie die Materialmenge so gering wie möglich. Um dies zu erreichen, helfen Sie Textur Satin. Zum Beispiel besteht fast die ganze Stadt in meinem Spiel aus 2-3 Atlanten. Es ist zu beachten, dass nicht alle mobilen Geräte mit großen Atlanten arbeiten können. Wenn Sie Geräte im Alter von 11 bis 13 Jahren unterstützen möchten, sollten Sie dies in Betracht ziehen. Ich habe beschlossen, die Unterstützung für Android unter 5.1 abzulehnen, da es sich meistens um alte Geräte handelt. Darüber hinaus läuft das Spiel aufgrund des linearen Renderns auf OpenGL 3.x.

5. Physik

Es ist einfach, FPS auf 10 zu senken. Es stellte sich heraus, dass sogar statische Objekte interagieren und an Berechnungen teilnehmen. Ich habe fälschlicherweise gedacht, dass statische physische Objekte (Objekte mit einer RigidBody-Komponente) bei Bedarf vollständig passiv sind. Ich wurde von dem alten Tutorial in die Irre geführt, das besagte, dass es RigidBody geben sollte, wo immer es einen Collider gibt. Jetzt sind alle meine statischen Objekte Static + BoxCollider. Wo ich Physik brauche, zum Beispiel Laternenpfähle, die niedergeschlagen werden können, denke ich, dass ich die RigidBody-Komponente bei Bedarf abschneiden muss.

Ebenen sind die Lebensader für die Optimierung. Deaktivieren Sie unnötige Interaktionen mithilfe von Ebenen. Verwenden Sie beim Neufassen Ebenenmasken. Warum brauchen wir zusätzliche Fehleinschätzungen? Denken Sie daran, dass es besser ist, einen einfachen übergeordneten Collider zu erstellen, um die Strahlen zu "fangen", wenn Ihr Objekt ein komplexes Kollidergitter hat und Sie mit einem Strahl darauf schießen. Je komplexer der Collider, desto mehr Fehleinschätzungen.

6. Okklusions-Keulung + Lod

Bei einer großen Szene ist das Keulen von Okklusionen unverzichtbar. Um Objekte (Bäume, Stangen usw.) in großer Entfernung zu deaktivieren, verwende ich Lod.

Bild

Bild

7. Objektpool

Alle vorgefertigten Implementierungen des Objektpools, die ich gefunden habe, werden instanziiert. Sie löschen und erstellen auch Objekte. Ich habe Angst, in all seinen Erscheinungsformen zu instanziieren. Langsamer Betrieb, der das Spiel mit einem mehr oder weniger großen Objekt einfriert. Ich habe mich für einen einfachen und schnellen Weg entschieden - mein gesamter Pool besteht aus physischen Spielobjekten, die ich bei Bedarf einfach aus- und wieder einschalte. Es trifft auf RAM, aber es ist besser. RAM für moderne Geräte von 1 GB, das Spiel verbraucht 300-500 MB.

Einfacher Pool zum Verwalten von Kampfbots:

  public List<Enemy> enemyPool = new List<Enemy>(); private void Start() { //    Enemy Transform enemyGameObjectContainer = Application.instance.objectPool.Find("Enemy"); //  enemyPool  for (int i = 0; i < enemyGameObjectContainer.childCount; i++) { enemyPool.Add(new Enemy() { Id = i, ParentRoomId = 0, GameObj = enemyGameObjectContainer.GetChild(i).gameObject }); } } public void SpawnEnemyForRoom(int roomId, int amount, Transform spawnPosition, bool combatMode) { //Stopwatch sw = new Stopwatch(); //sw.Start(); foreach (Enemy enemy in enemyPool) { if (amount > 0) { if (enemy.ParentRoomId == 0 && enemy.GameObj.activeSelf == false) { // id   enemy.ParentRoomId = roomId; enemy.GameObj.transform.position = spawnPosition.position; enemy.GameObj.transform.rotation = spawnPosition.rotation; enemy.AICombat = enemy.GameObj.GetComponent<AICombat>(); enemy.AICombat.parentRoomId = roomId; // id  enemy.AICombat.id = enemy.Id; //   enemy.GameObj.SetActive(true); //      if (combatMode) enemy.AICombat.ActivateCombatMode(); amount--; } } if (amount == 0) break; } } 

Datenbank


Ich benutze SQLite als Datenbank - bequem und schnell. Die Daten werden in Form einer Tabelle dargestellt, Sie können komplexe Abfragen durchführen. In der Klasse für die Arbeit mit der Datenbank 800 Zeilen, wenn. Ich kann mir nicht vorstellen, wie es in XML / JSON aussehen würde.

Probleme und Pläne für die Zukunft


Um von der Stadt in die „Räume“ zu ziehen, habe ich mich für die Implementierung von „Teleports“ entschieden. Der Spieler nähert sich der Tür, der Szenenraum wird geladen und der Spieler wird teleportiert. Dies erspart Ihnen die Aufbewahrung von Zimmern in der Stadt. Wenn Sie Räume in der Stadt implementieren, dh +15 Räume mit Füllung, erhöht sich der Speicherverbrauch auf mindestens 1 GB. Ich mag diese Implementierung nicht, sie ist nicht realistisch und bringt eine Reihe von Einschränkungen mit sich. Unity hat kürzlich eine Demo seiner Megacity gezeigt , es ist beeindruckend. Ich möchte das Spiel schrittweise auf esc übertragen und Technologie von Megacity verwenden, um Gebäude und Räumlichkeiten zu laden. Dies ist eine faszinierende und interessante Erfahrung. Ich denke, es wird eine wirklich lebendige Stadt. Warum habe ich keine asynchrone Ladeszene verwendet ? Es ist einfach, es funktioniert nicht, es gibt keine asynchrone Ladeszene in der Version 2018.3. Anfangs hoffte ich, bei der Planung einer Stadt eine asynchrone Ladeszene zu haben, aber wie sich herausstellt, friert das Spiel bei großen Szenen wie eine normale Ladeszene ein. Dies wurde im Unity-Forum bestätigt, man kann sich fortbewegen, aber Krücken werden benötigt.

Einige Statistiken:

Texturen: 304 / 374,3 MB
Maschen: 295 / 304,0 MB
Materialien: 101 / 148.0 KB (wahrscheinliche Diskrepanz hier)
Animationsclips: 24 / 2,8 MB
Audioclips: 22 / 30,3 MB
Aktiva: 21761
GameObjects in Szene: 29450
Gesamtzahl der Objekte in der Szene: 111645
Gesamtanzahl der Objekte: 133406
GC-Zuordnungen pro Frame: 70 / 2,0 KB
Insgesamt 4800 Zeilen C # -Code.

Jemand sagte mir, dass ein solches Spiel in einer Woche fertig sein kann. Vielleicht bin ich nicht produktiv, vielleicht ist diese Person talentiert, aber für mich selbst habe ich eines verstanden - es ist schwierig, solche Spiele alleine zu bauen. Ich wollte vor dem Hintergrund von lässigen „Fingern“ etwas Interessantes schaffen, es scheint mir, dass ich mich meinem Traum näherte.

Sie können die offene Beta hier testen und fühlen: play.google.com/store/apps/details?id=com.ag.mafiaProject01 (wenn die Assembly nicht funktioniert, müssen Sie sie ein wenig lieben, Updates kommen jede Nacht an). Ich hoffe, dass dies kein Werbelink ist, da diese Beta und Downloads mir keine Bewertung und Dividenden bringen. Außerdem glaube ich nicht, dass habr die Zielgruppe meines Spiels ist.

Screenshots:



Source: https://habr.com/ru/post/de439220/


All Articles