Vielen Dank an Vincent Quarles für die freundliche Unterstützung bei der Überprüfung dieses Artikels.
In diesem Tutorial werden wir die Implementierung von Speichern und Ladefunktionen in unserem Spiel beenden. Im vorherigen Tutorial zum Speichern und Laden von Player-Spieldaten in Einheit haben wir erfolgreich gespielte Daten wie Statistiken und Inventar gespeichert und geladen, aber jetzt werden wir uns mit den schwierigsten Teils befassen-Weltobjekte. Das endgültige System sollte an die Elder Scrolls -Spiele erinnern - jedes einzelne Objekt speichert genau dort, wo es war, für unbestimmte Zeit.
Wenn Sie ein Projekt zum Üben benötigen, ist hier eine Version des Projekts, das wir im letzten Tutorial abgeschlossen haben. Es wurde mit einem Paar interaktivierbarer Objekte im Spiel verbessert, die Gegenstände hervorbringen-einen Trank und ein Schwert. Sie können hervorgebracht werden und
( verdrängt ), und wir müssen ihren Zustand richtig speichern und laden. Eine fertige Version des Projekts (mit vollständig implementierter Save -System) finden Sie am Ende dieses Artikels.
Projekt GitHub Seite
Project Zip Download
Wir müssen das System des Speicherns und Ladens von Objekten aufschlüsseln, bevor wir es implementieren. In erster Linie brauchen wir eine Art von Level -Master
-Objekt, das Objekte hervorbringt und abtastet. Es muss gespeicherte Objekte in der Ebene laichen (wenn wir das Level laden und nicht neu anfangen), hat Despawn Objekte aufgegriffen, die Objekte benachrichtigt, die sie für die Rettung benötigen, und die Listen von Objekten verwalten. Klingt nach viel, also lass uns das in ein Flowdiagramm geben:
Grundsätzlich speichert die Gesamtheit der Logik eine Liste von Objekten auf einer Festplatte - die beim nächsten geladenen Level durchquert wird und alle Objekte davon beim Spielen hervorgebracht werden. Klingt einfach, aber der Teufel steht im Detail: Woher wissen wir, welche Objekte sparen sollen und wie wir sie zurückerregen?
Im vorherigen Artikel habe ich erwähnt, dass wir ein Delegat-Ereignis-System verwenden werden, um die Objekte zu benachrichtigen, die sie benötigen, um sich selbst zu retten. Erklären wir zunächst, was Delegierte und Ereignisse sind.
Sie können die offizielle Dokumentation und die offizielle Veranstaltungsdokumentation lesen. Aber keine Sorge: Selbst ich verstehe nicht viel Technobabble in offiziellen Dokumentation, also werde ich es in einfachem Englisch sagen:
Sie können sich einen Delegierten als Funktionsplan vorstellen. Es beschreibt, wie eine Funktion aussehen soll: wie sein Rückgabetyp ist und welche Argumente sie akzeptiert. Zum Beispiel:
public delegate void SaveDelegate(object sender, EventArgs args);
Dieser Delegate beschreibt eine Funktion, die nichts zurückgibt (void) und zwei Standardargumente für ein .NET/Mono -Framework akzeptiert: ein generisches Objekt, das einen Absender des Ereignisses darstellt, und eventuelle Argumente, mit denen Sie verschiedene Daten übergeben können. Sie müssen sich darüber keine Sorgen machen, wir können einfach als Argumente bestehen (Null, Null), aber sie müssen dort sein.
Wie stimmt das mit Ereignissen zusammen?
Sie können sich ein Ereignis als ein -Funktionsfeld
vorstellen. Es akzeptiert nur Funktionen, die mit dem Delegierten (einer Blaupause) übereinstimmen, und Sie können Funktionen zur Laufzeit, wie Sie möchten, ablegen und entfernen.
Dann können Sie jederzeit ein Ereignis auslösen, was bedeutet, dass Sie alle Funktionen ausführen, die sich derzeit in der Box befinden - gleichzeitig. Betrachten Sie die folgende Ereigniserklärung:
public event SaveDelegate SaveEvent;
Diese Syntax sagt: Deklarieren Sie eine öffentliche Veranstaltung (jeder kann es abonnieren - wir werden später dazu kommen), das Funktionen akzeptiert, wie von SavedElegate Delegate beschrieben (siehe oben), und es heißt SaveEvent.
Abonnieren eines Ereignisses bedeutet im Grunde genommen „eine Funktion in die Box aufzunehmen“. Die Syntax ist recht einfach. Nehmen wir an, unsere Veranstaltung ist in unserer bekannten GlobalObject-Klasse deklariert und wir haben eine Trankobjektklasse mit dem Namen Trankdropfen
, die ein Ereignis abonnieren muss:
//In PotionDroppable's Start or Awake function: GlobalObject.Instance.SaveEvent += SaveFunction; //In PotionDroppable's OnDestroy() function: GlobalObject.Instance.SaveEvent -= SaveFunction; //[...] public void SaveFunction (object sender, EventArgs args) { //Here is code that saves this instance of an object. }
Erklären wir die Syntax hier. Wir müssen zunächst eine Funktion haben, die den beschriebenen Delegiertenstandards entspricht. Im Skript des Objekts gibt es eine solche Funktion namens SaveFunction. Wir werden später unsere eigenen schreiben, aber vorerst nehmen wir einfach an, dass es sich um eine Arbeitsfunktion handelt, um das Objekt auf einer Festplatte zu speichern, damit es später geladen werden kann.
Wenn wir das haben, setzen wir diese Funktion einfach in die Box zu Beginn oder wach des Skripts und entfernen sie, wenn sie zerstört wird. (Es ist sehr wichtig, dass ein Abmessung wichtig ist: Wenn Sie versuchen, eine Funktion eines zerstörten Objekts aufzurufen, erhalten Sie zur Laufzeit Null -Referenz -Ausnahmen.) Wir tun dies, indem wir auf das deklarierte Ereignisobjekt zugreifen und einen Additionoperator verwenden, gefolgt vom Funktionsnamen.
Hinweis: Wir rufen keine Funktion mit Klammern oder Argumenten auf. Wir verwenden einfach den Namen der Funktion, sonst nichts.
Erklären wir also, was all dies letztendlich in einem Beispielfluss des Spiels tut.
Nehmen wir an, dass die Aktionen des Spielers durch den Spielfluss zwei Schwerter und zwei Tränke der Welt hervorgebracht haben (zum Beispiel öffnete der Spieler eine Truhe mit Beute).
Diese vier Objekte registrieren ihre Funktionen im Speichernereignis:
Nehmen wir jetzt an, der Spieler nimmt ein Schwert und einen Trank aus der Welt auf. Wenn die Objekte "aufgegriffen" werden, löst sie effektiv eine Änderung des Inventars des Spielers aus und zerstören sich dann (irgendwie ruiniert die Magie, ich weiß):
Und dann, nehme an, der Spieler beschließt, das Spiel zu retten - vielleicht müssen sie wirklich das Telefon beantworten, das jetzt dreimal klingelte (hey, du hast ein tolles Spiel gemacht):
Grundsätzlich ist die Funktionen in der Box nacheinander ausgelöst, und das Spiel geht nirgendwo hin, bis alle Funktionen erledigt sind. Jedes Objekt, das sich „selbst rettet“, schreibt sich im Grunde genommen in einer Liste auf - die bei der nächsten Spiellast durch das Level -Master -Objekt und alle Objekte, die in der Liste enthalten sind, in Betracht geprüft werden (laichig). Das war wirklich: Wenn Sie den Artikel bisher befolgt haben, sind Sie im Grunde bereit, ihn sofort implementieren zu können. Trotzdem werden wir hier zu einigen konkreten Codebeispielen gehen, und wie immer wird es am Ende des Artikels ein fertiges Projekt warten, wenn Sie sehen möchten, wie das Ganze aussehen soll.
Lassen Sie uns zunächst das vorhandene Projekt durchgehen und uns mit dem vertraut machen, was bereits im Inneren ist.
Wie Sie sehen, haben wir diese wunderschön gestalteten Kisten, die als Spawner für die beiden bereits erwähnten Objekte fungieren. In jedem der beiden Szenen gibt es ein Paar. Sie können das Problem sofort sehen: Wenn Sie die Szene übergehen oder f5/f9 zum Speichern/Laden verwenden, verschwinden die hervorgebrachten Objekte.
Die Box Spawners und die gelernten Objekte selbst verwenden einen einfachen interaktivierbaren Schnittstellenmechaniker, der uns die Möglichkeit ermöglicht, einen Raycast für diese Objekte zu erkennen, Text auf den Bildschirm zu schreiben und mit ihnen mit der [E] -Taste zu interagieren.
Nicht viel mehr ist vorhanden. Unsere Aufgaben hier sind:
Wie Sie sehen können, ist dies nicht so trivial wie man hoffen könnte, dass eine solche grundlegende Funktion wäre. Tatsächlich hat keine vorhandene Game Engine da draußen (CryeGine, UDK, Unreal Engine 4, andere) wirklich eine einfache Implementierung der Speichern/Ladefunktion. Dies liegt daran, dass Save -Mechaniker, wie Sie sich vorstellen können, für jedes Spiel wirklich spezifisch sind. Es gibt noch mehr, nur Objektkurse, die normalerweise gerettet werden müssen. Es sind Weltstaaten wie abgeschlossene/aktive Quests, Fraktionsfreundlichkeit/Feindseligkeit und sogar die aktuellen Wetterbedingungen in einigen Spielen. Es wird ziemlich komplex, aber mit der richtigen Grundlage der Sparenmechanik wird es einfach, es einfach mit mehr Funktionen zu verbessern.
Beginnen wir zuerst mit den einfachen Zügen - das Objektlisten. Die Daten unseres Players werden durch einfache Darstellung der Daten in der Klasse Serializable -Klasse gespeichert und geladen.
In ähnlicher Weise brauchen wir einige serialisierbare Klassen, die unsere Objekte darstellen. Um sie zu schreiben, müssen wir wissen, welche Eigenschaften wir sparen müssen - für unseren Spieler hatten wir viel zu sparen. Zum Glück werden Sie für die Objekte selten mehr als ihre Weltposition brauchen. In unserem Beispiel müssen wir nur die Position der Objekte und sonst nichts retten.
Um unseren Code gut zu strukturieren, beginnen wir mit einem einfachen Klassen am Ende unserer Serialisierungen:
public delegate void SaveDelegate(object sender, EventArgs args);
Sie fragen sich vielleicht, warum wir nicht nur eine Basisklasse verwenden. Die Antwort ist, dass wir konnten, aber Sie wissen nie wirklich, wann Sie bestimmte Elementeigenschaften hinzufügen oder ändern müssen, die Speichern benötigen. Außerdem ist dies für die Code -Lesbarkeit weitaus einfacher.
Inzwischen haben Sie vielleicht bemerkt, dass ich den Begriff dropsable
verwendet. Dies liegt daran, dass wir zwischen droppbaren (spawnbaren) Objekten und vermittelten Objekten unterscheiden müssen, die unterschiedlichen Regeln für das Speichern und Laichen folgen. Wir werden später dazu kommen.
Im Gegensatz zu den Daten des Spielers, in denen wir wissen, dass es zu einem bestimmten Zeitpunkt wirklich nur einen Spieler gibt, können wir mehrere Objekte wie Tränke haben. Wir müssen eine dynamische Liste erstellen und bezeichnen, in welcher Szene diese Liste gehört: Wir können die Objekte von Level2 in Level1 nicht hervorbringen. Dies ist einfach in Serialisierbaren zu tun. Schreiben Sie dies unter unserer letzten Klasse:
public delegate void SaveDelegate(object sender, EventArgs args);
Der beste Ort, um Instanzen dieser Listen zu erstellen, wäre unsere GlobalControl -Klasse:
public event SaveDelegate SaveEvent;
Unsere Listen sind für den Moment ziemlich gut zu gehen: Wir werden später von einem LevelMaster -Objekt auf sie zugreifen .
ah, endlich. Lassen Sie uns das berühmte Ereignis -Zeug implementieren.
in GlobalControl:
//In PotionDroppable's Start or Awake function: GlobalObject.Instance.SaveEvent += SaveFunction; //In PotionDroppable's OnDestroy() function: GlobalObject.Instance.SaveEvent -= SaveFunction; //[...] public void SaveFunction (object sender, EventArgs args) { //Here is code that saves this instance of an object. }
Wie Sie sehen, machen wir das Ereignis zu einer statischen Referenz, daher ist es logischer und einfacher, später zu arbeiten.
Eine endgültige Anmerkung zur Implementierung von Ereignissen: Nur die Klasse, die die Ereigniserklärung enthält, kann ein Ereignis abgeben. Jeder kann es abonnieren, indem er globalControl.savevent = ... zugreift, aber nur die GlobalControl -Klasse kann sie mit SaveEvent (NULL, NULL) abfeuern. Versuch, GlobalControl.savevent (Null, Null) zu verwenden; von anderer Stelle führt zu Compiler -Fehler!
Und das war es für die Event -Implementierung! Lassen Sie uns ein paar Sachen abonnieren!
Jetzt, da wir unser Ereignis haben, müssen unsere Objekte es abonnieren, oder mit anderen Worten Beginnen Sie mit dem Hören
zu einem Ereignis und reagieren Sie beim Brennen.
Wir brauchen eine Funktion, die ausgeführt wird, wenn ein Ereignis für jedes Objekt feuert. Gehen wir in die -Hekripte
zum Trankgang. Hinweis: Schwert hat noch nicht das Skript eingerichtet. Wir werden es gleich schaffen!
In Trinkanbindungen fügen Sie Folgendes hinzu:
[Serializable] public class SavedDroppablePotion { public float PositionX, PositionY, PositionZ; } [Serializable] public class SavedDroppableSword { public float PositionX, PositionY, PositionZ; }
[Serializable] public class SavedDroppableList { public int SceneID; public List<SavedDroppablePotion> SavedPotions; public List<SavedDroppableSword> SavedSword; public SavedDroppableList(int newSceneID) { this.SceneID = newSceneID; this.SavedPotions = new List<SavedDroppablePotion>(); this.SavedSword = new List<SavedDroppableSword>(); } }
public List<SavedDroppableList> SavedLists = new List<SavedDroppableList>();
public delegate void SaveDelegate(object sender, EventArgs args); public static event SaveDelegate SaveEvent;
Hier glänzt all unsere syntaktische Zuckerbeschichtung wirklich. Dies ist sehr lesbar, leicht zu verstehen und leicht zu ändern, wenn Sie es brauchen! Kurz gesagt, wir erstellen eine neue „Trankrepräsentation“ und speichern ihn in der tatsächlichen Liste.
Erstens ein kleines Stück Vorbereitung. In unserem vorhandenen Projekt haben wir eine globale Variable, die uns zeigt, ob die Szene geladen wird. Wir haben jedoch keine solche Variable, um uns mitzuteilen, ob die Szene durch die Verwendung der Tür übergangs
ist. Wir gehen davon aus, dass alle abgesetzten Objekte immer noch da sind, wenn wir in den vorherigen Raum zurückkehren, auch wenn wir unser Spiel irgendwo dazwischen nicht gespeichert/geladen haben.Um das zu tun, müssen wir kleine Veränderungen zur globalen Kontrolle vornehmen:
public delegate void SaveDelegate(object sender, EventArgs args);
In TransitionScript:
public event SaveDelegate SaveEvent;
Wir sind bereit, ein Levelmaster -Objekt zu erstellen, das normal funktioniert.
Jetzt müssen wir nur die Listen lesen und die Objekte von ihnen hervorbringen, wenn wir ein Spiel laden. Dies wird das Level -Master -Objekt tun. Erstellen wir ein neues Skript und nennen Sie es
LevelMaster
//In PotionDroppable's Start or Awake function: GlobalObject.Instance.SaveEvent += SaveFunction; //In PotionDroppable's OnDestroy() function: GlobalObject.Instance.SaveEvent -= SaveFunction; //[...] public void SaveFunction (object sender, EventArgs args) { //Here is code that saves this instance of an object. }
Das ist viel Code, also lass uns ihn aufschlüsseln.
Der Code wird nur zu Beginn ausgeführt, mit der wir bei Bedarf die gespeicherten Listen in GlobalControl initialisieren. Dann fragen wir die GlobalControl, ob wir eine Szene laden oder übergehen. Wenn wir die Szene neu beginnen (wie neues Spiel oder so), spielt es keine Rolle - wir laken keine Objekte.
Wenn wir eine Szene laden, müssen wir unsere lokale Kopie der Liste gespeicherter Objekte abrufen (nur um ein wenig Leistung beim wiederholten Zugriff auf GlobalControl, und
zu sparen Syntax lesbarer zu machen).
Als nächstes durchqueren wir einfach die Liste und laken alle Trankobjekte im Inneren. Die genaue Syntax für das Laichen ist im Grunde genommen eine der Instanziationsmethodenüberladungen. Wir müssen das Ergebnis der Instanziationsmethode in
gameObject
Hier wird das Objekt hervorgebracht: Wenn Sie andere Werte zur Spawn-Zeit ändern müssen, ist dies der Ort, um dies zu tun.
Wir müssen unseren Level -Master in jede Szene einfügen und den gültigen Vorbereitungen zuweisen:
Jetzt fehlen wir nur ein entscheidendes Stück: Wir müssen das Ereignis tatsächlich abfeuern, die Listen auf die Festplatte serialisieren und von ihnen lesen. Wir werden das einfach in unseren vorhandenen Speichern und Ladefunktionen in GlobalControl:
[Serializable] public class SavedDroppablePotion { public float PositionX, PositionY, PositionZ; } [Serializable] public class SavedDroppableSword { public float PositionX, PositionY, PositionZ; }
Dies scheint auch viel Code zu sein, aber der Großteil davon war bereits da. (Wenn Sie meinem vorherigen Tutorial verfolgt sind, werden Sie die Binärerialisierungsbefehle erkennen. Das einzige Neue hier ist die FireSavevent -Funktion und eine zusätzliche Datei, die unsere Listen speichert. Das ist es!
Wenn Sie das Projekt jetzt ausführen, werden die Trankobjekte jedes Mal korrekt gespeichert und geladen, wenn Sie f5 und f9
schlagen oder durch die Tür gehen (und jede Kombination von einer beliebigen Kombination von solches).Es gibt jedoch noch ein Problem zu lösen: Wir retten die Schwerter nicht.
Dies soll lediglich demonstrieren Erweiterung des Systems
Nehmen wir also an, Sie haben bereits ein neues Objektspawing-System vorhanden-wie wir es bereits mit den Schwertobjekten tun. Sie sind derzeit nicht viel interaktiv (jenseits der grundlegenden Physik), daher müssen wir ein Skript schreiben, das dem Trank ähnlich ist und es uns ermöglicht, ein Schwert aufzunehmen und es richtig zu speichern.Lassen Sie es uns zum Laufen bringen. Gehen Sie zu Assets> Skrips> Pickups und dort sehen Sie
PotiondroppableSkript. Erstellen Sie daneben ein neues Schwertdropp -Skript:
Vergessen Sie nicht die Implementierung „interaktivierbar“. Es ist sehr wichtig: Ohne es wird Ihr Schwert vom Kamera -Raycast nicht erkannt und bleibt uneingänglich. Überprüfen Sie auch, ob das Schwertprefab zu Elements Layer gehört. Andernfalls wird es vom Raycast erneut ignoriert. Fügen Sie nun dieses Skript dem ersten Kind des Schwertprefabs hinzu (das tatsächlich den Mesh -Renderer und andere Komponenten hat):
public delegate void SaveDelegate(object sender, EventArgs args);
Jetzt müssen wir sie hervorbringen. In Level Master unter unserer für die Schleife, die die Tränke hervorbringt:
public event SaveDelegate SaveEvent;
hinzufügen
Skript für ein Element erstellen, das sich abschließt, um Ereignisse zu speichernAber selbst so ist es bereits eine zuverlässige und solide Methode, um die Mechanik in Ihrem Spiel zu speichern/laden - wie sehr es sich auch von diesen
-Abbeispielen unterscheiden kannProjekte.
Wie versprochen ist hier das fertige Projekt, falls Sie es als Referenz benötigen, oder weil Sie irgendwo stecken geblieben sind. Das Save -System wird gemäß den Anweisungen dieses Tutorials und mit demselben Namensschema implementiert.
Implementierung der Autosave -Funktionalität in Unity 5 kann durchgeführt werden, indem ein Skript erstellt wird, das Ihr Spiel automatisch in regelmäßigen Abständen oder bei bestimmten Ereignissen speichert. Dieses Skript sollte dieselben Methoden zum Speichern von Daten als Handbuch Speichernystem verwenden.
Das obige ist der detaillierte Inhalt vonMastering Save- und Lastfunktionalität in Einheit 5. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!