Da vor kurzem jemand nach einer Möglichkeit zum Respawn gefragt hat und ich fanatisch LeGo angepriesen habe, dache ich mir, dass das ein geeignetes praktisches Beispiel sei, um den Umgeang mit PermMem ein wenig näher zu beleuchten. Aber Vorsicht, es folgt ein ziemlich langer Post. Wer also PermMem beherrscht oder einfach nicht so viel lesen will, sollte wohl gar nicht erst anfangen...
Als erstes müssen wir uns ein klares Ziel definieren bzw. aufschreiben, in unserem Fall das Respawn System:
"Unser System soll jedes Monster, das bestimmte Bedingungen erfüllt, nach einer bestimmten Zeit respawnen. Die Respawn-Zeit ist linear abhängig vom Level des Monsters und wird täglich (bzw. nächtlich in der SleepABit.d) geprüft."
Das hätten wir also. Da wir verzögert respawnen (Alles andere wäre für den Spieler ja eh frustrierend) müssen wir uns die relevanten Informationen merken. Daher sollten wir uns genau ansehen, was wir brauchen:
- Monster Typ oder Instanz (Wir wollen ja keinen Ork an Stelle eines Wolfes spawnen)
- Waypoint um zu wissen, wo der Respawn vollzogen werden soll
- Zeitpunkt, an dem der Respawn erfolgen soll
Der Übersicht halber nochmal in Daedalus formuliert:
Code:
class RespawnObject {
var int inst; //Die Monsterinstanz
var string wp; //Der Wegpunkt
var int respawnDay; // Der Tag des Respawns, wobei der erste Tag den Wert 0 hat... Genau wie Gothic.
};
Ich habe unserer Informationsstruktur (Im folgenden "Klasse"... Wer noch gar nichts über Klassen weiß, sollte sich mal in der Ikarus-Dokumentation umsehen) einfach mal den Namen
RespawnObject gegeben, Englisch ist schließlich hip.
In einem Objekt (einer Instanz) dieser Klasse können wir also alle nötigen Daten speichern.
Wenn jetzt ein Monster stirbt (und es unsere Bedingungen, z.B. kein Mensch, erfüllt), wollen wir ein Objekt erzeugen und es mit den entsprechenden Informationen füllen.
Wenn man sich jetzt fragt, warum es dazu PermMem braucht, ist die Frage völlig gerechtfertigt.
Ikarus bietet die Möglichkeit, Speicher zu allozieren und auch, den Speicher (bzw. einen Zeiger auf den Speicher) einer Instanz zuzuweisen. Aber (Und das ist ein großes Aber) Ikarus garantiert keinen sicheren Speicher, er ist bloß einen Frame sicher gültig. Anders formuliert: Wenn der Spieler lädt oder ähnliche Aktionen durchführt, führt das sehr oft dazu, dass der Speicher nicht mehr da steht, wo er soll (oder sogar ganz weg ist).
Genau das übernimmt aber PermMem: Es merkt sich den Speicherblock und "zwingt" den User im Gegenzug eine eindeutige Zahl ("Handle" genannt) zu benutzen, die PM immer einem Zeiger zuordnen kann. Wenn gespeichert wird, schreibt PM den Inhalt all dieser Speicherblöcke nach bestimmten Regeln (s. weiter unten oder in den
Beispielen) in eine eigene Speicherdatei; beim Laden wird diese Datei ausgelesen, interpretiert und die Daten werden wieder in den Speicher geschrieben.
Des Weiteren kann PM noch ein paar andere tolle Sachen, z.B. werden Objekte an Hand einer Instanz-Funktion initialisiert, Instanz-Funktionen meint sowas:
Code:
instance myNpc(C_NPC) {
Print("Ich bin eine Instanz-Funktion!");
};
Später erlaubt das, die Objekte nicht nur auf Grund ihrer Klasse zu unterscheiden sondern auch an Hand der Instanz, mit der sie erzeugt wurden, somit kann man sie unterschiedlich löschen oder speichern.
Wie auch immer, zurück zu unserem Problem: Das Respawn-System.
An geeigneter Stelle sollten wir so ein Objekt erzeugen - wir brauchen uns nichteinmal eine Referenz merken, weil wir alle wichtigen Objekte ja an ihrer Ursprungsinstanz identifizieren können (Wird später noch klar). Ab jetzt geht das Scripten wirklich los:
Wir erzeugen als erstes ein Respawn-Objekt mit dem Befehl new(). Aber was erwartet new() für Parameter? Ein Blick ins
Wiki hilft:
Code:
int new(instance inst)
Gut, new() will also eine Instanz... Aber wir haben ja gar keine Instanz? Unser Code sieht ja bisher so aus:
Code:
class RespawnObject {
var int inst; //Die Monsterinstanz
var string wp; //Der Wegpunkt
var int respawnDay; // Der Tag des Respawns, wobei der erste Tag den Wert 0 hat... Genau wie Gothic.
};
New() möchte eine Instanz als Parameter, da damit das Objekt initialisiert wird (werden kann). Eigentlich wird einfach die Instanz-Funktion (s.o.) ausgeführt. Da wir die Instanz-Funktion nicht allzu sinnvoll parametrisieren können, nehmen wir einfach eine leere:
Code:
instance RespawnObject@(RespawnObject);
// oder
instance RespawnObject@(RespawnObject){};
Code:
func void AddToRespawnArray(var c_npc slf) {
var int hndl; hndl = new(RespawnObject@);};
Nun, das ist noch nicht vollständig und das könnt ihr euch vermutlich denken. Wir müssen noch die Informationen in unser Objekt schreiben. Dafür brauchen wir die Funktionen
get() oder
getPtr(). Es ist einfacher, direkt ein Objekt zu nutzen anstatt erst den Zeiger in ein Objekt zu verwandeln, daher werden wir get() benutzen. Wir definieren also eine Variable des Typs RespawnObject, das geht wie mit Integern und Strings:
Code:
var RespawnObject myRespawnObject;
Dieser Variable weisen wir dann einfach unser Handle zu (korrekt:
get(hndl);) und können ohne Probleme die Informationen in das Objekt schreiben. Also:
Code:
func void AddToRespawnArray(var c_npc slf) {
var int hndl; hndl = new(RespawnObject@);
var RespawnObject myRespawnObject; myRespawnObject = get(hndl);
myRespawnObject.inst = Hlp_GetInstanceID(slf);
myRespawnObject.wp = slf.spawnPoint;
myRespawnObject.respawnDay = Wld_GetDay() + (slf.level/10)+2; // Irgendeine Formel
};
Und das war es eigentlich auch schon, damit sind alle wichtigen Informationen verstaut. Jetzt müssen wir die Funktion nur noch an einem sinnvollen Ort aufrufen - und was eignet sich da besser als die ZS_Dead()?
Richtig, nichts.
Es ist eigentlich völlig egal, wo wir unsere Funktion in der ZS_Dead() aufrufen, aber der Übersicht halber machen wir es ganz am Anfang. Da wir nicht alle Monster wiederbeleben wollen, packen wir das Ganze auch noch in eine Abfrage:
Code:
func int MeetsRespawnCondition(var c_npc slf) {
return (slf.guild > GIL_SEPERATOR_HUM); // Überprüft bisher bloß, ob 'slf' nicht menschlich ist
};
func void ZS_Dead ()
{
if (MeetsRespawnCondition(self)) {
AddToRespawnArray(self);
};
[...]
};
Die Abfrage ist noch nicht so toll und euch fallen bestimmt noch andere wichtige Sachen rein, aber ich will ja bloß ein Konzept vorstellen.
Okay... Ein kleiner Rückblick: Wir haben uns die wichtigen Informationen erfolgreich gemerkt...
Und mehr nicht. 'Ne ganze Menge Arbeit für so eine kleine Aufgabe, nicht?
Wenn wir zurück schauen, sehen wir aber: Das waren eigentlich bloß ein paar Zeilen, kaum eine zweistellige Anzahl. Sobald man PM verstanden habt (Und ich bin mir sicher das werdet ihr nach diesem Tutorial), kann man das sehr schnell tippen. Machen wir uns aber nun an den eigentlichen Teil unseres Systems: Das Respawnen. Um einen geeigneten Zeitpunkt zum Auslösen des Respawns kümmern wir uns später. Wir wollen jetzt also eine Funktion, die testet, ob der aktuelle Tag größer gleich dem "Respawn-Tag" ist und gegebenfalls das Monster in die Welt einfügen, das ist auch gar nicht so schwierig, der Code erklärt sich, mitsamt Kommentaren, eigentlich von selbst:
Code:
func void CheckRespawns() {
ForEachHndl(RespawnObject@, _CheckRespawns);/* Dieser Part bedarf vielleicht einer kurzen Erklärung:
Mit ForEachHndl() führe ich eine angegebene Funktion (2. Parameter) für alle Objekte/Handles
einer angegebenen Ursprungsinstanz (1. Parameter) aus. So kann ich ganz einfach
alle RespawnObjects überprüfen :) */
};
Code:
func int _CheckRespawns(var int hndl) { // In diesem Parameter steht, für welches Handle die Funktion gerade ausgeführt wird
var RespawnObject myRespawnObject; myRespawnObject = get(hndl);
//Jetzt haben wir unser Objekt!
if (myRespawnObject.respawnDay <= Wld_GetDay()) { // Der Tag des Respawns ist gekommen! \o/
Wld_InsertNpc(myRespawnObject.inst, myRespawnObject.wp); // Daher fügen wir einfach den NPC an seinem WP ein :)
// Allerdings müssen wir nun unser Objekt auch entfernen, sonst würde es ja beim nächsten Mal wieder eingefügt!
// Ich werde daher einfach mal die Funktion RemoveRespawnObject() aufrufen - wie die aussehen muss, schauen wir später.
RemoveRespawnObject(hndl);
return rContinue; // Wir wollen schließlich weiterhin alle Handles durchlaufen - bis wir alle haben
};
So weit, so gut, das war nicht so kompliziert, jetzt müssen wir uns aber um die
RemoveRespawnObject() kümmern. Die ist aber eigentlich ganz simpel: Wir müssen bloß unser RespawnObject löschen (Und damit den Speicher wieder freigeben), darum kümmert sich PM aber fast ganz von alleine:
Code:
func void RemoveRespawnObject(var int hndl) {
delete(hndl); // Alles weitere macht PM dann selber. Unter anderem wird versucht, Respawn_Object_Delete() aufzurufen
};
Wir hätten auch direkt bloß delete(hndl) aufrufen können, das macht keinen Unterschied.
Das rufen wir jetzt noch in der PC_Sleep() (Datei: Scripts\Content\Story\Dialog_Mobsis\SleepABit.d) auf, optimalerweise nachdem die Zeit geändert wurde:
Code:
func void PC_Sleep (var int t)
{
AI_StopProcessInfos(self); // [SK] ->muss hier stehen um das update zu gewährleisten
PLAYER_MOBSI_PRODUCTION = MOBSI_NONE;
self.aivar[AIV_INVINCIBLE]=FALSE;
if (Wld_IsTime(00,00,t,00))
{
Wld_SetTime (t,00);
}
else
{
t = t + 24;
Wld_SetTime (t,00);
};
CheckRespawns();
[...]
};
Der Kern unseres Systems ist damit geschaffen und wir näherns uns (rapide!) dem Ende, eigentlich möchte ich bloß noch eine (in diesem Fall unnötige) Sache aufzeigen:
Klassen in Gothic haben leider nicht genug Informationen, um sie (immer) vernünftig speichern zu können, Zeiger unterscheiden sich z.B. nicht von normalen Ganzzahlwerten. Daher bietet LeGo sog.
structs an. Das ist dem eigentlichen Begriff aus anderen Programmiersprachen ein wenig entfremdet, in unserem Fall ist das bloß ein String, der den Aufbau der Klasse beschreibt. Dabei wird einfach Stück für Stück der "Typ" einer Objekt-Eigenschaft angegeben, wobei einfache Werte (Func, float, int, string) einfach als 'auto' (für 'automatisch') referenziert werden. Man kann auch eine Anzahl angeben, wenn mehrere Eigenschaften des selben Typs aufeinander folgen. In unserem Fall sähe das so aus:
Code:
const string RespawnObject_Struct = "auto|3";
// oder
const string RespawnObject_Struct = "auto auto auto";
Hätten wir in unserer RespawnObject-Klasse am Ende noch einen Zeiger auf ein weiteres Respawn-Object, sähe das so aus:
Code:
const string RespawnObject_Struct = "auto|3 RespawnObject*";
Der Sternchen-Operator zeigt, wie aus anderen Programmiersprachen gewohnt, einen Zeiger an.
Es ist auch möglich, die Speicherregeln komplett von Hand vorzugeben, aber das werde ich in diesem Tutorial nicht behandeln und sollte aus der Dokumentation im Wiki auch ersichtlich werden.
Und damit sind wir endlich am Ende angekommen! Was dem ein oder anderen wie ein Haufen Arbeit erscheint haben mag, war letztendlich ganz wenig - ca. 75 Zeilen, wobei davon einige Leerzeilen oder Kommentare waren.
Abschließende Worte: Ich habe nicht großartig gegengelesen, wenn also jemand noch Fehler findet (Syntaxfehler in Script oder auch Sprache), behebe ich das gerne. Semantisch sollte das im Groben und Ganzen eigentlich korrekt sein, ich gebe allerdings zu, es nichtmal durch den Parser gejagt zu haben. Wenn es nicht funktioniert, seht es einfach als zusätzliche Aufgabe, es zu beheben