Was (nicht) Sie wissen müssen, um Spiele auf Unity zu erstellen



Unity ist eine Spiel-Engine mit einem Schwellenwert, der weit von Null entfernt ist (im Vergleich zu demselben Game Maker Studio). In diesem Artikel werde ich Ihnen erläutern, auf welche Probleme ich beim Beginn des Studiums gestoßen bin und welche Lösungen ich gefunden habe. Ich werde solche Momente am Beispiel meines 2D-Puzzlespiels für Android beschreiben (das hoffentlich bald auf dem Play Market veröffentlicht wird).

Ich gebe nicht vor, wahr zu sein, und ich fordere Sie nicht auf, nach sich selbst zu wiederholen. Wenn Sie den besten Weg kennen, zeige ich Ihnen nur, wie es geht, und vielleicht schafft jemand, der gerade erst anfängt, Unity kennenzulernen, sein Indie-Gamedev-Meisterwerk mit weniger Arbeit.

Ich bin Konstrukteur von Kraftwerken, habe mich aber schon immer für das Codieren interessiert und bin mit einigen Programmiersprachen vertraut. Daher sind wir uns einig, dass zum Erstellen von Spielen auf Unity:
  • Sie müssen ein wenig C # oder JavaScript kennen (zumindest die C-förmige Syntax).

Alles, was unten geschrieben wird, ist kein Unity-Tutorial, von dem genug ohne mich im Netzwerk gezüchtet wurden. Im Folgenden werden die schwierigen Momente zusammengefasst, die beim Erstellen Ihres ersten Projekts in Unity auftreten können.

Es lohnt sich zu warnen, dass die bereitgestellten Skripte den größten Teil der Spiellogik (die "Geschäftsgeheimnisse" darstellt) weglassen, aber ihre Leistung als Beispiele wurde überprüft.

Problem Eins - Orientierung



Orientierungssperre
Die erste Schwierigkeit, die bei mir auftrat, war, dass ich nicht darauf geachtet habe, die visuelle Oberfläche für die Bildschirmorientierung zu optimieren. Die Lösung ist die einfachste - wenn Sie keine Änderung der Bildschirmausrichtung für das Gameplay benötigen, ist es besser, sie zu blockieren. Keine Notwendigkeit für übermäßige Flexibilität, Sie schreiben ein Indie-Spiel, kein Projekt auf der anderen Seite von einer Million Dollar. Warum tonnenweise bedingte Übergänge und Ankerwechsel, wenn das Spiel in Portrait (zum Beispiel) besser aussieht. Hier können Sie die Bildschirmausrichtung sperren:
Bearbeiten> Projekteinstellungen> Player


Unterschiedliche Berechtigungen
Es ist auch wichtig, die visuelle Oberfläche mit unterschiedlichen Auflösungen in der ausgewählten Ausrichtung zu testen. Vergessen Sie beim Testen nicht, dass Geräte mit einem Verhältnis von 4: 3 (gut oder 3: 4) vorhanden sind, damit Sie sicher 768 x 1024 (oder 1024 x 768) hinzufügen können.

Bessere Positionierung
Um die Position und den Maßstab von Spielobjekten anzupassen, ist es besser, die Rechtecktransformation zu verwenden.


Problem Zwei - KOMMUNIKATION


Ich hatte ein ähnliches Problem aufgrund der Tatsache, dass ich den Spielentwickler zum ersten Mal über Game Maker Studio kennengelernt habe, wo das Skript ein vollwertiger Teil des Spielobjekts ist und sofort vollen Zugriff auf alle Komponenten des Objekts hat. Unity verfügt über gemeinsame Skripte, von denen nur Instanzen zum Objekt hinzugefügt werden. Vereinfacht gesagt weiß das Skript nicht direkt, auf welchem ​​Objekt es gerade ausgeführt wird. Daher müssen Sie beim Schreiben von Skripten die Initialisierung der Schnittstellen für die Arbeit mit den Komponenten eines Objekts oder mit den Komponenten anderer Objekte berücksichtigen.

Wir trainieren an Katzen
In meinem Spiel gibt es ein GameField-Objekt, auf der Bühne gibt es nur eine Instanz davon, es gibt auch ein gleichnamiges Skript. Das Objekt ist für die Anzeige der Spielpunktzahl und die Wiedergabe des gesamten Spieltons verantwortlich. Meiner Meinung nach ist es für den Speicher wirtschaftlicher (im Allgemeinen verfügt das Spiel nur über drei Audioquellen - eine Hintergrundmusik, zwei weitere Soundeffekte). Das Skript löst die Probleme beim Speichern eines Spielkontos, beim Auswählen von AudioClip zum Abspielen von Sound und für einige Spielelogiken.

Lassen Sie uns näher auf den Klang eingehen, da dieses Beispiel die Interaktion des Skripts mit den Komponenten des Objekts leicht zeigt.

Natürlich sollte das Objekt das GameField.cs-Skript selbst und die AudioSource-Komponente haben, in meinem Fall zwei ganze (später wird klar, warum).

Wie bereits erwähnt, weiß das Skript nicht, dass das Objekt über eine AudioSource-Komponente verfügt. Daher deklarieren und initialisieren wir die Schnittstelle (wir gehen derzeit davon aus, dass es nur eine AudioSource gibt):
private AudioSource Sound; void Start(){ Sound = GetComponent<AudioSource> (); } 

Die GetComponent-Methode <Komponententyp> () gibt die erste Komponente des angegebenen Typs vom Objekt zurück.

Zusätzlich zu AudioSource benötigen Sie mehrere AudioClip:
 [Header ("Audio clips")] [SerializeField] private AudioClip OnStart; [SerializeField] private AudioClip OnEfScore; [SerializeField] private AudioClip OnHighScore; [SerializeField] private AudioClip OnMainTimer; [SerializeField] private AudioClip OnBubbMarker; [SerializeField] private AudioClip OnScoreUp; 

Im Folgenden werden die Befehle in eckigen Klammern für Inspector`a benötigt. Weitere Details finden Sie hier .



Jetzt hat das Skript in Inspector neue Felder, in die wir die erforderlichen Sounds ziehen.


Erstellen Sie als Nächstes eine SoundPlay-Methode in dem Skript, das AudioClip enthält:
 public void PlaySound(AudioClip Clip = null){ Sound.clip = Clip; Sound.Play (); } 

Um Sound im Spiel zu spielen, rufen wir diese Methode zum richtigen Zeitpunkt mit dem Clip auf.

Es gibt ein signifikantes Minus dieses Ansatzes: Es kann immer nur ein Sound gleichzeitig abgespielt werden. Während des Spiels müssen jedoch möglicherweise zwei oder mehr Sounds abgespielt werden, mit Ausnahme der Hintergrundmusik, die ständig abgespielt wird.

Um Kakophonie zu vermeiden, empfehle ich, die Möglichkeit der gleichzeitigen Wiedergabe von mehr als 4-5 Sounds (vorzugsweise maximal 2-3) zu vermeiden. Ich meine, kurze Sounds erster Ordnung (Sprung, Münze, Spielerschuss ...). Für Hintergrundgeräusche ist es besser, eine eigene Quelle zu erstellen Ton auf dem Objekt, das dieses Rauschen verursacht (wenn Sie 2D-3D-Ton benötigen) oder einem Objekt, das für alle Hintergrundgeräusche verantwortlich ist (wenn "Lautstärke" nicht benötigt wird).

In meinem Spiel müssen nicht mehr als zwei AudioClips gleichzeitig abgespielt werden. Für die garantierte Wiedergabe beider hypothetischer Sounds habe ich dem GameField-Objekt zwei AudioSource hinzugefügt. Um die Komponenten im Skript zu bestimmen, verwenden wir die Methode
 GetComponents<_>() 

Dies gibt ein Array aller Komponenten des angegebenen Typs vom Objekt zurück.

Der Code sieht folgendermaßen aus:
 private AudioSource[] Sound; //    void Start(){ Sound = GetComponents<AudioSource> (); //  GetComponents } 

Die meisten Änderungen wirken sich auf die PlaySound-Methode aus. Ich sehe zwei Versionen dieser Methode: "universal" (für eine beliebige Anzahl von AudioSource in einem Objekt) und "plump" (für 2-3 AudioSource nicht die eleganteste, aber weniger ressourcenintensive).

Die "ungeschickte" Option für zwei AudioSource (ich habe es verwendet)
 private void PlaySound(AudioClip Clip = null){ if (!Sound [0].isPlaying) { Sound [0].clip = Clip; Sound [0].Play (); } else { Sound [1].clip = Clip; Sound [1].Play (); } } 

Sie können auf drei oder mehr AudioSource strecken, aber die Anzahl der Bedingungen verschlingt alle Leistungseinsparungen.

Option "Universal"
 private void PlaySound(AudioClip Clip = null){ foreach (AudioSource _Sound in Sound) { if (!_Sound.isPlaying) { _Sound.clip = Clip; _Sound.Play (); break; } } } 


Zugriff auf eine fremde Komponente
Auf dem Spielfeld gibt es mehrere Instanzen des Fishka-Fertighauses, wie z. B. einen Spielchip. Es ist so aufgebaut:
  • Übergeordnetes Objekt mit seinem SpriteRenderer;
    • Untergeordnete Objekte mit ihrem SpriteRenderer.

Untergeordnete Objekte sind dafür verantwortlich, den Körper des Chips, seine Farbe und zusätzliche veränderbare Elemente zu zeichnen. Der Elternteil zeichnet einen Markierungsrand um den Chip (der aktive Chip muss im Spiel hervorgehoben werden). Das Skript befindet sich nur für das übergeordnete Objekt. Um untergeordnete Sprites zu verwalten, muss das übergeordnete Skript diese Sprites angeben. Ich habe es so organisiert - im Skript habe ich Schnittstellen für den Zugriff auf die SpriteRenderer-Kinder erstellt:
 [Header ("Graphic objects")] public SpriteRenderer Marker; [SerializeField] private SpriteRenderer Base; [Space] [SerializeField] private SpriteRenderer Center_Red; [SerializeField] private SpriteRenderer Center_Green; [SerializeField] private SpriteRenderer Center_Blue; 

Jetzt enthält das Skript im Inspektor zusätzliche Felder:


Durch Ziehen und Ablegen von Kindern in die entsprechenden Felder erhalten wir Zugriff auf sie im Skript.

Anwendungsbeispiel:
 void OnMouseDown(){ //        Marker.enabled = !Marker.enabled; } 


Das Skript eines anderen aufrufen
Sie können nicht nur fremde Komponenten bearbeiten, sondern auch auf das Skript eines Objekts eines Drittanbieters zugreifen und mit dessen öffentlichen Variablen, Methoden und Unterklassen arbeiten.

Ich werde ein Beispiel für das bereits bekannte GameField-Objekt geben.

Das GameField-Skript verfügt über eine öffentliche Methode FishkiMarkerDisabled (), die zum „Entfernen“ eines Markers von allen Chips auf dem Feld benötigt wird und beim Setzen eines Markers beim Klicken auf einen Chip verwendet wird, da nur einer aktiv sein kann.

Im Fishka.cs-Skript ist SpriteRenderer Marker öffentlich, dh es kann von einem anderen Skript aus darauf zugegriffen werden. Fügen Sie dazu die Deklaration und Initialisierung von Schnittstellen für alle Instanzen der Fishka-Klasse im GameField.cs-Skript hinzu (beim Erstellen eines Skripts wird die gleichnamige Klasse darin erstellt), ähnlich wie bei mehreren AudioSource:
 private Fishka[] Fishki; void Start(){ Fishki = GameObject.FindObjectsOfType (typeof(Fishka)) as Fishka[]; } public void FishkiMarkerDisabled(){ foreach (Fishka _Fishka in Fishki) { _Fishka .Marker.enabled = false; } } 

Fügen Sie im Skript Fishka.cs die Deklaration und Initialisierung der Schnittstelle der GameField-Klasseninstanz hinzu. Wenn Sie auf das Objekt klicken, rufen Sie die FishkiMarkerDisabled () -Methode dieser Klasse auf:
 private GameField gf; void Start(){ gf = GameObject.FindObjectOfType (typeof(GameField)) as GameField; } void OnMouseDown(){ gf.FishkiMarkerDisabled(); Marker.enabled = !Marker.enabled; } 

Auf diese Weise können Sie zwischen Skripten (oder vielmehr Klassen) verschiedener Objekte interagieren.


Problem drei - KEEPERS


Kontoführer
Sobald so etwas wie ein Konto im Spiel erscheint, besteht das unmittelbare Problem darin, dass es sowohl während als auch außerhalb des Spiels gespeichert wird. Ich möchte auch Aufzeichnungen führen, um den Spieler zu ermutigen, es zu übertreffen.

Ich werde keine Optionen in Betracht ziehen, wenn das gesamte Spiel (Menü, Spiel, Kontoabhebung) in einer Szene erstellt wird, da dies erstens nicht der beste Weg ist, um das erste Projekt zu erstellen, und zweitens sollte meiner Meinung nach die anfängliche Ladeszene sein . Daher sind wir uns einig, dass das Projekt vier Szenen enthält:
  1. Loader - Eine Szene, in der das Hintergrundmusikobjekt initialisiert wird (mehr wird später angezeigt) und Einstellungen aus dem Speicher geladen werden.
  2. Menü - eine Szene mit einem Menü;
  3. Spiel - Spielszene;
  4. Partitur - die Szene der Partitur, Aufzeichnung, Rangliste.


Hinweis: Die Ladereihenfolge der Szene wird unter Datei> Build-Einstellungen festgelegt.

Während des Spiels gesammelte Punkte werden in der Score-Variablen der GameField-Klasse gespeichert. Um Zugriff auf die Daten zu erhalten, wenn Sie zur Scores-Szene gehen, erstellen Sie eine öffentliche statische Klasse ScoreHolder, in der wir eine Variable zum Speichern des Werts und eine Eigenschaft zum Abrufen und Festlegen des Werts dieser Variablen deklarieren (die Methode wurde von Apocatastas ausspioniert ):
 using UnityEngine; public static class ScoreHolder{ private static int _Score = 0; public static int Score { get{ return _Score; } set{ _Score = value; } } } 

Eine öffentliche statische Klasse muss keinem Objekt hinzugefügt werden, sie ist in jeder Szene in jedem Skript sofort verfügbar.

Beispiel für die Verwendung in der GameField-Klasse in der Szenenübergangsmethode:
 using UnityEngine.SceneManagement; public class GameField : MonoBehaviour { private int Score = 0; //     ,         Scores void GotoScores(){ ScoreHolder.Score = Score; //   ScoreHolder.Score   SceneManager.LoadScene (“scores”); } } 

Auf die gleiche Weise können Sie dem ScoreHolder während des Spiels ein Rekordkonto hinzufügen, das jedoch beim Beenden nicht gespeichert wird.

Einstellungsbewahrer
Betrachten Sie das Beispiel zum Speichern des Werts der Booleschen Variablen SoundEffectsMute, je nachdem, in welchem ​​Zustand das Spiel Soundeffekte aufweist oder nicht.

Die Variable selbst wird in der öffentlichen statischen Klasse SettingsHolder gespeichert:
 using UnityEngine; public static class SettingsHolder{ private static bool _SoundEffectsMute = false; public static bool SoundEffectsMute{ get{ return _SoundEffectsMute; } set{ _SoundEffectsMute = value; } } } 

Die Klasse ähnelt ScoreHolder, man könnte sie sogar zu einer kombinieren, aber meiner Meinung nach sind das schlechte Manieren.

Wie Sie dem Skript entnehmen können, wird _SoundEffectsMute standardmäßig als false deklariert. Bei jedem Spielstart gibt SettingsHolder.SoundEffectsMute false zurück, unabhängig davon, ob der Benutzer es zuvor geändert hat oder nicht (es wird über die Schaltfläche auf der Menübühne geändert).

Variablen speichern
Für eine Android-Anwendung ist es am besten, die PlayerPrefs.SetInt-Methode zum Speichern zu verwenden (weitere Informationen finden Sie in der offiziellen Dokumentation ). Es gibt zwei Optionen, um den Wert von SettingsHolder.SoundEffectsMute in PlayerPrefs beizubehalten. Nennen wir sie "einfach" und "elegant".

Der "einfache" Weg (für mich so) ist die OnMouseDown () -Methode der Klasse der oben genannten Schaltfläche. Der gespeicherte Wert wird in dieselbe Klasse geladen, jedoch in die Start () -Methode:
 using UnityEngine; public class ButtonSoundMute : MonoBehaviour { void Start(){ //    ,  PlayerPrefs    bool switch (PlayerPrefs.GetInt ("SoundEffectsMute")) { case 0: SettingsHolder.SoundEffectsMute = false; break; case 1: SettingsHolder.SoundEffectsMute = true; break; default: //    default SettingsHolder.SoundEffectsMute = true; break; } } void OnMouseDown(){ SettingsHolder.SoundEffectsMute = !SettingsHolder.SoundEffectsMute; //    ,  PlayerPrefs    bool if (SettingsHolder.SoundEffectsMute) PlayerPrefs.SetInt ("SoundEffectsMute", 1); else PlayerPrefs.SetInt ("SoundEffectsMute", 0); } } 


Die "elegante" Methode ist meiner Meinung nach nicht die richtigste, weil erschweren die Wartung des Codes, aber es ist etwas drin, und ich kann nicht anders, als es zu teilen. Ein Merkmal dieser Methode ist, dass der Setter der SettingsHolder.SoundEffectsMute-Eigenschaft zu einem Zeitpunkt aufgerufen wird, der keine hohe Leistung erfordert, und mit PlayerPrefs (Lesen - Schreiben in eine Datei) geladen werden kann (oh, Horror). Ändern Sie die öffentliche statische Klasse SettingsHolder:

 using UnityEngine; public static class SettingsHolder { private static bool _SoundEffectsMute = false; public static bool SoundEffectsMute{ get{ return _SoundEffectsMute; } set{ _SoundEffectsMute = value; if (_SoundEffectsMute) PlayerPrefs.SetInt ("SoundEffectsMute", 1); else PlayerPrefs.SetInt ("SoundEffectsMute", 0); } } } 

Die OnMouseDown-Methode der ButtonSoundMute-Klasse vereinfacht Folgendes:
 void OnMouseDown(){ SettingsHolder.SoundEffectsMute = !SettingsHolder.SoundEffectsMute; } 


Es lohnt sich nicht, den Getter mit Lesen aus einer Datei zu laden, da er an einem leistungskritischen Prozess beteiligt ist - in der PlaySound () -Methode der GameField-Klasse:
 private void PlaySound(AudioClip Clip = null){ if (!SettingsHolder.SoundEffectsMute) { //      “”  (. ) if (!Sound [0].isPlaying) { Sound [0].clip = Clip; Sound [0].Play (); } else { Sound [1].clip = Clip; Sound [1].Play (); } } } 


Auf die oben beschriebene Weise können Sie die Speicherung beliebiger Variablen im Spiel organisieren.


Fünftes Problem - EINES FÜR ALLE


Diese Musik wird ewig sein
Früher oder später steht jeder vor einem solchen Problem, und ich war keine Ausnahme. Wie geplant wird Hintergrundmusik auch in der Menüszene abgespielt. Wenn sie nicht ausgeschaltet ist, werden Menü-, Spiel- und Partituren ohne Unterbrechung in den Szenen abgespielt. Wenn das Objekt, das Hintergrundmusik "spielt", in der Menüszene installiert ist, wird es beim Aufrufen der Spielszene zerstört und der Ton verschwindet. Wenn Sie dasselbe Objekt in die Spielszene einfügen, wird die Musik nach dem Übergang zuerst abgespielt. Es stellte sich heraus, dass die Lösung die DontDestroyOnLoad-Methode (Objektziel) verwendet, die in der Start () -Methode der Klasse platziert ist, deren Skriptinstanz das Musikobjekt hat. Erstellen Sie dazu das Skript DontDestroyThis.cs:
 using UnityEngine; public class DontDestroyThis: MonoBehaviour { void Start(){ DontDestroyOnLoad(this.gameObject); } } 

Damit alles funktioniert, muss das „musikalische“ Objekt root sein (auf derselben Hierarchieebene wie die Hauptkamera).

Warum Hintergrundmusik im Lader
Der Screenshot zeigt, dass sich das „musikalische“ Objekt nicht in der Menüszene, sondern in der Loader-Szene befindet. Dies ist eine Maßnahme, die dadurch verursacht wird, dass die Menüszene mehr als einmal geladen werden kann (nach der Partiturszene, dem Übergang zur Menüszene) und jedes Mal, wenn sie geladen wird, ein weiteres „musikalisches“ Objekt erstellt wird und das alte nicht gelöscht wird. Dies kann wie im Beispiel der offiziellen Dokumentation erfolgen , aber ich habe mich entschlossen, die Tatsache auszunutzen, dass die Loader-Szene garantiert nur einmal geladen wird.

Die Hauptprobleme, auf die ich bei der Entwicklung meines ersten Spiels auf Unity vor dem Hochladen auf den Play Market gestoßen bin (ich habe noch kein Entwicklerkonto registriert), wurden erfolgreich beendet.

PS
Wenn die Informationen nützlich waren, können Sie den Autor unterstützen und er wird schließlich ein Android-Entwicklerkonto registrieren.

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


All Articles