Willkommen Anonymous, es sind 43 User online. [ Anzeigen ]


[RPG]Board Neverwinter Nights NWN Scripting


[ Login ] [ Registrieren ] [ Abo ] [ Suche ] [ Profil ] [ Hilfe ]


Operatoren: zauriel, Avantenor
Moderatoren: NWN Moderat, Camael, IRQ
[ ANTWORTEN ]
[ FAQ ]
Autor Druckansicht   Thema: [HOWTO]: Hinweise zum besseren Programmier-Stil
Seite: 1
Carsten

Profil anschauen ]

Gilde/Clan:
City of Arabel

erstellt am 30.06.2004 um 11:30 Uhr   Homepage besuchen    Zitatantwort        #924339

Ich dachte mir, ich fange einmal einen Thread mit Tipps und Tricks an, damit nicht immer die selben Fragen auftauchen, insbesondere ein paar Sachen in Richtung Code-Stil und Lesbarkeit. Wann immer mir etwas einfällt, schreibe ich mehr - wer weitere Dinge hat die hier passen kann sich gerne anschliessen.
Da ich in der Regel an einem englischen Modul mit dem englischen Toolset arbeite sind die meisten Begriffe entweder in Englisch oder frei von mir übersetzt - ich bitte um Nachsicht.

Zunächst verweise ich hier auf diesen Thread: [Externer Link - Bitte einloggen oder registrieren], dann muss ich die Links nicht auch noch wiederholen. Auch interessant ist [Externer Link - Bitte einloggen oder registrieren], "Fehler die man vermeiden kann"
Nur einen, denn der ist der ultimative Link was NWN Script angeht: [Externer Link - Bitte einloggen oder registrieren]

- Templates
Im Verzeichnis "scripttemplates" liegen diverse .txt-Dateien, deren Inhalt man durch Anwählen der Datei aus dem "Templates"-Reiter ganz rechts in das aktuelle Skript übernehmen kann. Ihr könnt dort ganz einfach eigene Dateien einfügen oder die existierenden editieren und damit im Toolset viel Tipparbeit sparen.
Ich verwende es gerne für Spawn- und Userdefined-Skripte sowie für die Tag-Basierten Ereignisse die seit HotU möglich sind.

- Variablen ordentlich benennen
Benennt eure Variablen so, dass sie einleuchten. Glücklicherweise hat sich die Notation eingebürgert den Typ der Variable in den Namen zu integrieren, statt "PC" also "oPC", da der PC normalerweise ein Objekt ist. Ich verwende in der Regel:
i - für Integers (oft sieht man hier auch n, das ist Geschmackssache)
o - für Objekte
loc - für Locations/Orte (viele verwenden nur l, aber das lese ich immer als "Long", deswegen ziehe ich loc vor)
v - für Vektoren
d - für Double/Fliesskommazahlen
Manchmal verwende ich auch "b" wenn eine Variable nur einen Booleschen Wert enthält, aber das drücke ich normalerweise im Namen aus.

Zwei große Fehler passieren hier gern (auch einem Profi-Skripter ). Einerseits oberflächliche Namensgebung, andererseits fehlerhafte Benennung. Oberflächlich wäre zum Beispiel "oObjekt" oder "iZahl". Versucht, in den Namen den Sinn der Variablen einfließen zu lassen. Also besser "oBoeserAltar" oder "iGroesseDerParty". Wie ihr seht verwende ich Groß/Kleinschreibung um die Namen lesbarer zu machen - ich mag Unterstriche nicht weil sie die Länge der Variablen aufblähen und der Code schwerer lesbar wird, wenn man scrollen muss. Was findet ihr lesbarer:

//** das: 
if (i<=5) { ... } 
//** oder: 
if (iGroesseDerParty<=5) { ... } 



Bei der ersten Zeile muß ein Kommentar dazu damit man den Sinn versteht (etwa "//** i enthält die Anzahl der Spieler in der Gruppe"), die zweite Zeile erklärt sich selbst. Lässt man den Kommentar weg, muss jemand der den Code liest erst einmal zurückblättern und sehen was eigentlich in die Variable i geschrieben wird. Und da derjenige, der den Code liest vermutlich ihr seid, nur eben ein paar Wochen später, tut euch selbst den Gefallen und programmiert leserlich.

Der zweite große Fehler resultiert aber daraus. Stellt euch folgendes OnEnter-Skript eines Auslösers vor:

object oPC = GetEnteringObject(); 
object oHeini = GetObjectByTag("Heini"); 
if (GetLocalInt(oHeini, "iHabeBereitsGesprochen") == 1) 
	return: 
SetLocalInt(oHeini, "iHabeBereitsGesprochen", 1); 
AssignCommand(oPC, ActionStartConversation( oHeini, ... ));


Relativ selbsterklärend, oder? Der PC, der den Auslöser betritt wird genötigt, mit einem Heini zu sprechen, aber nur einmal. Aber was passiert, wenn ein NPC in den Auslöser kommt? Auch dieser führt den Code aus, und sorgt unter Umständen dafür, dass das Gespräch ausgeführt wird bevor ihr jemals in die Nähe kommt.
Hier ist es leicht durch die Namensgebung "oPC" anzunehmen, dass das betretende Objekt IMMER ein Spieler ist.

- Fehlersuche
Das ist das Härteste der ganzen Sache. Irgend etwas klappt nicht, und keiner weiss warum. Abgesehen davon den Code theoretisch durchzugehen und abzuklopfen bleiben auch zwei Möglichkeiten: den Skript-Debugger oder Debug-Ausgaben. Zu ersterem kann ich leider nicht viel sagen, er klingt sehr hilfreich, aber ich hatte noch nicht die Muße ihn mir anzuschauen.
Debug-Ausgaben hingegen sind einfach. Fügt einfach überall wo ihr denkt dass etwas schiefgehen kann eine SpeakString-Anweisung ein. Dadurch erhaltet ihr nicht nur die Information was passiert, sondern auch wer das Skript ausführt (nämlich derjenige, den das Spiel as Sprecher ausgibt). Nehmen wir das "Heini"-Skript.

object oPC = GetEnteringObject(); 
object oHeini = GetObjectByTag("Heini"); 
SpeakString( "DEBUG: Bin im Heini-Skript, oPC=" + GetName(oPC) + ", oHeini=" + GetName(oHeini), TALKVOLUME_SHOUT ); 
if (GetLocalInt(oHeini, "iHabeBereitsGesprochen") == 1) 
	return: 
SetLocalInt(oHeini, "iHabeBereitsGesprochen", 1); 
SpeakString( "DEBUG: Heini-Skript 2, oPC=" + GetName(oPC) + " redet mit oHeini=" + GetName(oHeini), TALKVOLUME_SHOUT ); 
AssignCommand(oPC, ActionStartConversation( oHeini, ... ));


Das Rufen ist deswegen nötig, damit man die Ereignisse auch mitbekommt, wenn man in einem anderen Gebiet ist. Nun ist das natürlich lästig wenn man den Code korrigiert hat und nun überall die SpeakString()s entfernen muss. Deswegen habe ich mir ein include geschrieben, das etwa so aussieht:

/** cs__inc_debug 
 ** 
 ** Just a wrapper to shout crap all across the area 
 **/ 
void DebugMessage( string sText )  
{ 
	SpeakString( "DEBUG: " + sText, TALKVOLUME_SHOUT ); 
}


Wenn ich also sicher bin dass mein Code funktioniert oder ich meine Ruhe haben will, kommentiere ich den SpeakString in diesem include aus, übersetze alle Skripte neu und alle Ausgaben verstummen. Ich könnte auch ein globales Suchen/Ersetzen starten dass "DebugMessage" durch "//DebugMessage" ersetzt - dadurch spart man dann die Rechenzeit für den Funktionsaufruf.

Genauso kann man das Ganze natürlich mit SendMessageToPC machen - das tue ich in der Regel, wenn der Code auf einen aktiven Server kommt. Irgendwo gibt es dann einen Hebel, mit dem man sich als Debugger-PC identifiziert, und ab da wird man zugetextet.

/** cs__inc_debug 
 ** 
 ** Debugging with a little more style 
 **/ 
object g_oDebugPC; 
 
void DebugMessage( string sText )  
{ 
	if (GetLocalInt(GetModule(), "iDebugIsActive") == 1) 
		SendMessageToPC( g_oDebugPC, "DEBUG: " + sText ); 
} 
 
//** as the name implies, set or reset debug mode. oUser will be made the guy who receives any debug messages 
void ToggleDebugMode( object oUser ) 
{ 
	g_oDebugPC = oUser; 
	if (GetLocalInt(GetModule(), "iDebugIsActive") == 1) 
		SetLocalInt(GetModule(), "iDebugIsActive", 0); 
	else 
		SetLocalInt(GetModule(), "iDebugIsActive", 1); 
}


- Namen für die Skripts
Die Namensgebung, die der Wizard vorschlägt ist Müll. "SC" oder "AT" und eine Nummer? Wer soll daraus in einer Woche noch schlau werden? Dies ist besonders schlecht wenn ihr Sachen mehrfach braucht - sagen wir, einen Bluff-Test mit immer dem gleichen Schwierigkeitsgrad. Nennt ihr das Ding "SC_045" seid ihr irgendwann besser bedient, wenn ihr den Wizard erneut anwerft und den Bluff-Test NOCHMAL erzeugen lasst (diesmal unter "SC_073" gespeichert). Irgendwann besteht die Hälfte eures Moduls aus Code, der ettliche Male vorhanden ist. Nennt das Ding "Check_Bluff_25", "Check_Persuade_10", "HasFeat_Dodge" oder "Race_IsElf" dann findet ihr es viel leichter wieder, das Modul bleibt kleiner und die Zeit zum Übersetzen steigt nicht unnötig.
Skripte, die zusammenhängen nenne ich auch ähnlich, mit dem gemeinsamen Teil am Start damit die Skripte im "Öffnen"-Fenster zusammen stehen. Also Namen wie "Drache_Reden", "Drache_IstTot", "Drache_Spawn" erlauben es mir, einen möglichen Fehler schnell auf eine Gruppe von Skripten festzunageln.

- Includes / Libraries, gemeinsamer Code eben
Viele Dinge muss man mehrfach tun. Zum Beispiel habe ich es mit Code zu tun, der einem Spieler (z.B. wenn er getötet wird) alle Gegenstände wegnimmt und in einen Beutel. Diesen Code brauche ich im OnPlayerDeath-Skript, im OnPlayerDying-Skript (denn wenn der Spieler blutet, soll ein Kamerad die Möglichkeit haben ein Heilpaket aus dem Rucksack des Sterbenden zu nehmen) und im Skript das die Verhaftung des Spielers durch die Stadtwache erledigt (hier landet der Besitz des Spielers dann in der Asservatenkammer). Jetzt kann ich den Code einmal schreiben und dann in die anderen Skripte kopieren. Nur: wenn ich jetzt Gegenstände einbaue, die nicht verloren gehen sollen, muss ich das auch an drei Stellen korrigieren! Viel einfacher ist es, den Code dann in eine eigene Datei auszulagern und sie mit #include in die drei Skripte einzubinden. Einziges Problem dabei ist dass es etwas unübersichtlicher wird und Änderungen an der include-Datei nur wirksam werden, wenn man das aufrufende Skript neu übersetzt.

- Einrückungen
Der Code wird deutlich lesbarer, wenn ihr Blöcke einrückt. Ich ziehe es vor, nach jeder geschweiften Klammer (oder da wo eine kommen sollte) einen Tabulator einzurücken, manche machen das mit Leerzeichen. Nehmt aber genügend und seit sorgfältig! Vergleicht die verschiedenen Schreibweisen:

void ToggleDebugMode( object oUser ) 
{ 
	g_oDebugPC = oUser; 
	if (GetLocalInt(GetModule(), "iDebugIsActive") == 1) 
		SetLocalInt(GetModule(), "iDebugIsActive", 0); 
	else 
		SetLocalInt(GetModule(), "iDebugIsActive", 1); 
} 
 
void ToggleDebugMode( object oUser ) 
{ 
g_oDebugPC = oUser; 
if (GetLocalInt(GetModule(), "iDebugIsActive") == 1) 
SetLocalInt(GetModule(), "iDebugIsActive", 0); 
else 
SetLocalInt(GetModule(), "iDebugIsActive", 1); 
} 
 
void ToggleDebugMode( object oUser ) 
{ 
	g_oDebugPC = oUser; 
	if (GetLocalInt(GetModule(), "iDebugIsActive") == 1) 
		SetLocalInt(GetModule(), "iDebugIsActive", 0);	else 
	SetLocalInt(GetModule(), "iDebugIsActive", 1); 
}


Alle tun dasselbe, aber die Leserlichkeit ist doch sehr unterschiedlich. Und das letzte Beispiel ist sehr gefährlich, zeigt es doch wie man mit Formatierung lügen kann.

- Umbrüche
Scheut euch nicht, eine lange Zeile zu teilen. Wie in einem C-Compiler auch ist es dem NWScript-Compiler egal wo der Code steht, so lange kein Strichpunkt kommt. Rückt die Zeile aber sinnvoll ein!

- Tags und ResRefs
Ich denke jeder ist einmal über das Problem gestolpert, Tag und ResRef durcheinandergebracht zu haben. Ich achte immer darauf, dass beide gleich sind. Dadurch bin ich in der Länge der Tags zwar beschränkt, aber in der Regel reichen die verfügbaren Zeichen aus.
Besondere Sorgfalt lasst bitte walten, wenn ihr eine Kopie einer existierenden Kreatur/eines Gegenstandes anlegt. Die ResRef der Kopie ist dann wird dann durch Anhängen einer Nummer erzeugt, und das wiederrum erschwert das Lesen des Codes. Vergleich:
sResRef = "skeleton002"; 
sResRef = "skeleton_axe"; 
CreateObject(..., sResRef, ..);

Wenn ich ein Object komplett neu anlege (mit dem entsprechenden Wizard), gebe ich ihm immer einen Vornamen, der zur ResRef werden soll, und ändere den Vornamen später. So wird aus "Drache_QuestGeber" später "König Harald der Müde", er hat aber weiterhin Tag und ResRef "Drache_QuestGeber", und wenn ich später entscheide dass doch lieber der Hofnarr die Drachenbeseitigung beauftragt muss ich meinen Code nicht ändern, nur Name und Aussehen des NPCs.

Fortsetzung folgt, wenn ich die Muße dazu habe
Moderator
Camael

Profil anschauen ]

Gilde/Clan:
Open-Sourcerer

erstellt am 30.06.2004 um 15:23 Uhr   Homepage besuchen    Zitatantwort        #924459

Ich war mal so frei den Thread fest zu pinnen und den Titel etwas konkreter zu gestalten.

--
Am I waiting to be seized and turned into a brand?
A chip beneath my skin, ready to be scanned?
Am I merely a supply, meeting some demand?
Or is this our lullaby in someone's masterplan?

[Externer Link - Bitte einloggen oder registrieren]

Carsten

Profil anschauen ]

Gilde/Clan:
City of Arabel

erstellt am 01.07.2004 um 11:24 Uhr   Homepage besuchen    Zitatantwort        #924977

Ich war mal so frei den Thread fest zu pinnen und den Titel etwas konkreter zu gestalten.


Heh. Danke schön . Hier kommt auch schon die Fortsetzung.


Kommentierung
Unbedingt! Immer! Egal wie trivial, es besteht immer die Gefahr dass ihr vergesst wofür euer Code gut ist. Denkt daran, ihr kommentiert dafür, dass ihr den Code in einiger Zeit noch versteht, oder dass jemand anderes einfacher nachschauen kann was eigentlich das Skript macht. Dies ist besonders wichtig wenn ihr in einem Team arbeitet. Ich habe mir angewöhnt, jedes Skript mit einem Header zu versehen (so wie Bioware auch, aber meiner ist etwas kompakter da er auf meinem Namensgebungssystem aufbaut). Er sieht so aus:

/** <Name des Skripts> 
 ** 
 ** <Beschreibung was es tut> 
 ** 
 ** <Versionsgeschichte> 
 **/


Der Name ist nicht ganz so wichtig, aber hilfreich wenn man das Skript einmal ausdruckt.
Die Beschreibung hingegen ist essentiell. Haltet sie kurz, aber prägnant (das schließt sich meist leider aus). In den ersten Zeilen beschreibt den Zweck des Skriptes, denn diese Zeilen sind es, die ihr in den Vorschaufenstern des Dialogeditors seht.
Die Versionsgeschichte hilft bei der Fehlersuche und kann bei Programmierteams dazu beitragen denjenigen auszumachen, an den man sich bei Verständnisfragen wendet. Sie sollte den Namen der Person enthalten die das Skript änderte, das Datum wann es geschah und eine kurze Beschreibung. Ich verwende inzwischen ein Versionskontrollsystem (RCS wen es interessiert), das mir dabei hilft.

Innerhalb des Codes sollten auch Kommentare vorhanden sein, lieber zu viele als zu wenig.
Vermeidet offensichtliche Kommentare. Das:

//** i0 wird um Eins erhöht 
i0 = i0 + 1;


macht keinen Sinn, jeder halbwegs vernünftige Mensch braucht hier keinen Kommentar. Besser ist:

//** wir haben einen weiteren Feind gefunden, speichere das 
i0 = i0 + 1;


und am allerbesten:

//** wir haben einen weiteren Feind gefunden, speichere das 
iFeinde = iFeinde + 1;


Wobei in der letzten Version der Kommentar nicht mehr zwingend nötig ist, der Code "spricht für sich selbst".

Trickreiche Programmierung
Manchmal kann es sinnvoll sein, den Code kompakter zu gestalten, indem man Dinge zusammenfasst. Ich mag es allerdings nicht so sehr, da es den Code schwerer lesbar macht. Das Beispiel "DebugToggle" hätte ich auch so kodieren können:

void ToggleDebugMode( object oUser )  
{  
    g_oDebugPC = oUser;  
    SetLocalInt(GetModule(), "iDebugIsActive", 1 - GetLocalInt(GetModule(), "iDebugIsActive"));  
}


Der Nachteil ist jedoch, dass sich der Leser dann überlegen muss, was eigentlich passiert wenn man einen Wert von Eins abzieht. Ausserdem besteht eine mögliche Fehlerquelle, sollte aus irgendeinem Grund iDebugIsActive einen anderen Wert als Null oder Eins haben. Ähnlich ist ein solcher Konstrukt:

int nDiff = FloatToInt((fCR < 1.0) ? 1.0 : fCR);


Er funktioniert prima, aber ich finde es leichter zu lesen wenn man schreibt

if (fCR < 1.0)  
    nDiff = 1; 
else 
    nDiff = FloatToInt(fCR);



Verwendung von Objekten
Was macht mehr Sinn, Objekte anlegen oder immer die Funktion aufzurufen, die das Objekt liefert? Der Code:

... = GetLocalInt(GetModule(), "iDebugIsActive");


könnte auch so geschrieben werden:

object oMod = GetModule(); 
... = GetLocalInt(oMod, "iDebugIsActive");


Was man tut ist meiner Meinung nach Geschmackssache. Durch das Anlegen einer Variable oMod wird man etwas flexibler, aber der Code wird meist minimal langsamer. "Meist", weil ich nicht sagen kann wie aufwändig ein Aufruf von GetModule() ist. Normalerweise verwende ich Variablen, sobald ich einen Wert häufiger als einmal verwende, was praktisch immer vorkommt . Die Verwendung von Variablen vereinfacht es auch, den Code später in includes auszulagern wenn man feststellt dass man ihn häufiger verwenden will.


Jetzt geht es etwas mehr in die technische Seite des NWN-Programmierens, und etwas weniger Stil.


Das vermaledeite OBJECT_SELF
Objekte sind das A und O bei NWScript, auch wenn ich die Sprache selber nicht objektorientiert nennen kann. Aber ALLES mit ganz wenigen Ausnahmen ist ein Objekt. Spieler, NPCs, Gegenstände, Bereiche, das Modul selbst...
Was ist nun OBJECT_SELF? Nun, streng genommen ein Zeiger auf das Objekt, das für die Ausführung des gerade laufenden Skriptes zuständig ist. Zu wissen, wer wann welches Skript ausführt ist schon die halbe Miete im Skripten. Eine Liste zu machen ist zu aufwändig, aber in der Regel könnt ihr davon ausgehen dass derjenige, der eine Aktion auslöst auch im OBJECT_SELF gespeichert wird. Das kann der NPC sein wenn es sich um Skripte im "Text erscheint wenn..." oder "Vorgenommene Aktion"-Feld handelt, der Zaubernde (PC, NPC oder auch Mobiliar wenn man es dazu nötigt) im Falle von Zauberskripts oder sogar das Modul selbst - dies ist bei den Modul-Ereignisskripts der Fall.
Wenn ihr euch unsicher seid: Einfach ein SpeakString einbauen das GetName(OBJECT_SELF) ausgibt, dann wisst ihr es.

Was zeichnet also ein Objekt aus? Zunächst einmal ist ein Objekt schlicht ein Bereich im Speicher. Darin stehen dann die Eigenschaften des Objekts; diese sind je nach Objekt unterschiedlich. Ein Spieler-Objekt hat Name und Stufe, ein Gegenstands-Objekt Name und diverse Ereignis-Skripte und so weiter. Wenn wir in NWScript mit Objekten hantieren, hantieren wir aber eigentlich nicht mit dem Objekt selber, sondern mit einem Zeiger auf den Speicherbereich. Das Objekt selber wird entweder von der Engine angelegt, mit dem Toolset platziert oder per CreateObject erzeugt. Alle Funktionen, die als Rückgabewert "object" haben, liefern also einen Zeiger auf einen Speicherbereich. Es sind beliebig viele Zeiger auf dieses Objekt möglich, wirklich Sinn macht es aber nicht. Der Code

object oPC = GetFirstPC(); 
object oKerl = GetFirstPC(); 
GiveXP(oPC, 10); 
GiveXP(oKerl, 10); 
oKerl = oPC; 
GiveXP(oKerl, 10);


gibt dem ersten PC auf dem Server dreissig Erfahrungspunkte, da oPC und oKerl beide auf den gleichen Speicherbereich zeigen. Ich sehe so etwas oft wenn mehrere Skripte zusammenkopiert werden:

object oA = OBJECT_SELF; 
//** tue etwas mit oA 
 : 
object oB = OBJECT_SELF; 
//** tue etwas anderes mit oB 
 :


Es ist nicht wirklich kritisch, aber verwirrend und fehlerträchtig.

Bestimmen des Ziels
In der Regel hat ein Skript den Zweck, den Status eines Objekts zu verändern oder auf Aktionen eines Objekts zu reagieren. Dazu muss das Skript aber erst einmal wissen, welches Objekt betroffen ist. In den meisten Fällen wirkt hier der generische Zeiger OBJECT_SELF, den euch netterweise die Engine zur Verfügung stellt. Das klappt aber nicht immer, zum Beispiel wenn ihr feststellen wollt wer welches Objekt abgelegt hat. Im modulweiten "OnUnacquireItem"-Skript zeigt OBJECT_SELF leider auf das Modul, und das ist nicht das was gewünscht ist. Ganz besonders störend ist dies bei den genialen "Tag-based" Skripts (die werde ich später noch erklären) die Georg Zoeller in HotU eingeführt hat. Da diese technisch gesehen von den modulweiten Skripts aus gestartet werden, enthält OBJECT_SELF leider nichts Verwertbares.

Hier kommen dann die Get...-Funktionen ins Spiel, GetLastPCSpeaker(), GetLastAttacker(), GetLastItemLost() und so weiter und so fort. Ganz einfach gesagt, wenn es keine Get...-Funktion gibt die zur Aufgabe passt, ist die Aufgabe unlösbar oder nur extrem schwer zu realisieren. Als Beispiel: es gibt keine GetSecondLastPCSpeaker()-Funktion mittels der ein NPC sagen kann: "Ja, vor Dir hat ... mit mir gesprochen". Hier kann man sich nur so behelfen, dass man aus den existierenden Funktionen seine eigenen zusammenstrickt, zum Beispiel indem man jeden PC speichert der mit dem NPC gesprochen hat. Hier setzt dann auch in der Regel unsere Aufgabe ein, nämlich aus den verfügbaren Funktionen ein Modul zusammenzustricken.

Bitte kein GetFirstPC()!
Nachdem ich es oben verwendet habe, verdamme ich diese Funktion gleich. In vielen Modulen wird diese Funktion anstelle der "richtigen" Get...()-Funktionen verwendet. Sicher, im Einzelspielermodus ist es egal ob ihr GetEnteringObject() oder GetFirstPC() verwendet, tatsächlich ist es sogar sicherer mit GetFirstPC() da das IMMER einen PC betrifft. Ihr verbaut euch aber damit den Weg, das Modul jemals mit Freunden zu spielen - und das ist das, was mir am meisten Spass macht. Ausserdem öffnen sich ganz neue Welten in der Debugging-Technik wenn ihr euer Modul in einem Server starten könnt und euch als Spieler UND DM einloggt (ihr braucht zwar zwei Computer und zweimal NWN, aber durch einen DM Avatar könnt ihr einiges beeinflussen was als Spieler selbst mit Konsolenkommandos nicht geht.)
Ich empfehle also dringend immer nach der richtigen Funktion zu suchen wenn ihr auf einen PC zugreifen müsst. Für alle normalen Ereignisse gibt es passende Get...()-Funktionen, auch wenn man oft etwas suchen muss. GetFirstPC() sollte nur dann verwendet werden, wenn wirklich eine Schleife durch alle PCs erforderlich ist.

Ich liebe HotU!
Man kann einem Objekt auch eigene Eigenschaften (meist "lokale Variablen" genannt) zuweisen. Dazu dienen die Set/GetLocal...()-Funktionen. Die Anweisung

SetLocalInt(GetModule(), "iDebugIsActive", 1);


verleiht dem Modul-Objekt die neue Eigenschaft "iDebugIsActive" und weist ihr den Wert Eins zu. Dadurch ist es möglich, beliebigen Objekten beliebige Variablen zuzuorden. Ich liebe HotU deswegen, weil ich nun im Toolset Variablen an alle Objekte anhängen kann. Das macht spezielle Spawn-Skripte unnötig und erlaubt extrem flexibles Kodieren. Ein Beispiel sind die Geheimtüren, die in SoU hinzukamen. Jede Art von Tür (Holz oder Stein, normale oder Falltüre usw.) hat ihr eigenes Skript, so dass das Modul sieben Skripte enthält die sich nur durch die ResRef der zu erzeugenden Tür unterscheiden. Als ich die Aufgabe bekam "Wir wollen eine Geheimtür, die für Mitglieder einer bestimmten Gruppe automatisch erscheint, von anderen durch eine Suchen-Probe gefunden werden kann und die nach einer gewissen Zeit wieder verschwindet" dachte ich zuerst an eine Mammut-Aufgabe, aber durch die Variablen die ich im Toolset setzen kann wurde es sehr simpel. Mit diesem Skript im OnEnter eines Auslösers habe ich eine PW-taugliche Tür, deren Verhalten ich durch die Variablen steuern kann:

/** cs_gen_secret 
 ** 
 ** A little more generic secret door trigger. It does check the presence of 
 ** an item as well and can spawn various secret doors. Also, the door 
 ** disappears after a set time. This is better for PW use. 
 ** 
 ** Simply use the HotU feature of the toolset to attach variables named 
 **  sAutoItemTag - if the enterer has an item with that tag, he sees the door automatically 
 **  sDoorResRef - this is the resref of the object to spawn 
 **  iSpamDelay - seconds that must pass between checks to prevent spamming. Negative 
 **               numbers mean only one check per person. Default 24 seconds. 
 **  iDontPlaySound - if 1, does not play the "I found something" voice bite 
 **  iDontDestroyObject - if 1, the door stays around indefinitely 
 **  fDestroyDelay - time until destroying the door again 
 ** 
 ** Doors possible: 
 **  x0_sec_door1, x0_sec_tdoor1 - wooden door and trapdoor 
 **  x0_sec_door2, x0_sec_tdoor2 - stone door and trapdoor 
 **  x0_sec_door3 - big door 
 **  x0_sec_grate1, x0_sec_grate2 - stone and metal grate 
 ** 
 ** The rest is default. The door will be spawned at position "LOC_<tag of trigger> 
 ** and it will lead to "DST_<tag of trigger>" 
 ** 
 ** $Log: cs_gen_secret.nss $ 
 ** 
 ** Revision 1.1  2004/04/08 12:46:52  Carsten 
 ** Initial revision 
 ** 
 **/ 
 
#include "x0_i0_secret" 
 
 
void main() 
{ 
    //** shortcut, no need to work if the door is there already 
    if (GetIsSecretItemRevealed()) {return;} 
 
    //** only PC should detect doors. I am unsure about henchmen and familiars however 
    object oEntered = GetEnteringObject(); 
    if (GetIsPC(oEntered)) 
    { 
        //** prevent spamming the trigger 
        if (GetLocalInt( oEntered, GetTag(OBJECT_SELF) + "_tried" )) 
            return; 
        int iDelay = GetLocalInt(OBJECT_SELF, "iSpamDelay" ); 
        if (iDelay == 0) 
            iDelay = 24; 
        SetLocalInt( oEntered, GetTag(OBJECT_SELF) + "_tried", 1 ); 
        if (iDelay > 0) 
            DelayCommand( iDelay * 1.0, SetLocalInt( oEntered, GetTag(OBJECT_SELF) + "_tried", 0 )); 
 
        //** here's the check whether the PC is carrying a special item. If not, 
        //** he may detect the door the hard way. 
        string sItemTag = GetLocalString(OBJECT_SELF, "sAutoItemTag" ); 
        if ( GetIsObjectValid(GetItemPossessedBy(oEntered, sItemTag )) 
            //** the hard way. Stolen from BioWare  
         ||  DetectSecretItem(oEntered)) 
        { 
            //** let the PC say something so the player notices the door 
            if (GetLocalInt(OBJECT_SELF, "iDontPlaySound") == 0) 
                AssignCommand(oEntered, PlayVoiceChat(VOICE_CHAT_LOOKHERE)); 
 
            //** create the door using BioWare functions 
            string sDoorResRef = GetLocalString(OBJECT_SELF, "sDoorResRef" ); 
            if (sDoorResRef == "") 
                sDoorResRef = "x0_sec_door2"; 
            RevealSecretItem(sDoorResRef); 
 
            //** if not disabled, destroy the door again 
            if (GetLocalInt(OBJECT_SELF, "iDontDestroyObject") == 0) 
            { 
                float fDelay = GetLocalFloat(OBJECT_SELF, "fDestroyDelay"); 
                if (fDelay <= 0.0)  fDelay = 45.0; 
                DestroyObject( GetSecretItemRevealed(), fDelay ); 
                DelayCommand( fDelay, SetLocalInt(OBJECT_SELF, "IS_REVEALED", FALSE)); 
            } else { 
                //** this is for the quest system - it will kill anything with this integer 
                SetLocalInt( GetSecretItemRevealed(), "iQuestSpawned" , 1 ); 
            } 
        } 
    } 
}


Wie ihr seht, mache ich hier ausgiebig Gebrauch von lokalen Variablen, die ich auf die verschiedenen Objekte setze. Das Skript ist deswegen so kompakt weil ich viele Funktionen von BioWare "klaue" - dazu dient das include "x0_i0_secret". Bevor ihr etwas kodiert, sucht einmal im Lexikon oder einem Modul, die Chancen sind recht gut dass sich schon jemand damit befasst hat.

Stevit

Profil anschauen ]

erstellt am 01.07.2004 um 11:48 Uhr     ICQ 52457258  Zitatantwort        #924980

Wiederkehrende Strukturen:
Vermeidet längere Blöcke von Zeilen, die ansich immer weider dieselbe Struktur haben.
Zwar erscheint einem selbst beim Schreiben alles sehr simpel und übersichtlich, doch später beim Debuggen ist es äusserst unerfreulich wenn man in z.B. 70 Zeilen überprüfen muss ob man eine einzelne Zeile vergessen hat, oder sich irgendwo ein Tippfehler eingeschlichen hat.
Und so etwas passiert schnell.

Versucht stattdessen euch die immer wiederkehrende Struktur nutzbar zumachen um den Code durch Schleifen zu vereinfachen. Wenn sich ein Fehler einschleicht, so lässt sich dieser dann wesentlich leichter finden und beheben, da alle Fälle die selben Codezeilen nutzen.


Beispiel:
 
object oPC = GetEnteringObject();  
 
object oGear = GetItemInSlot(INVENTORY_SLOT_ARMS, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_BELT, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_BOLTS, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_BOOTS, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_CHEST, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_CLOAK, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_HEAD, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_LEFTHAND, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_LEFTRING, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_NECK, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_RIGHTHAND, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_RIGHTRING, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_ARROWS, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_BOLTS, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear);  
oGear = GetItemInSlot(INVENTORY_SLOT_BULLETS, oPC);  
if(GetIsObjectValid(oGear)) DestroyObject(oGear); 



Sicherlich komplett zulässig und lauffähig. Doch habe ich nun alle Slots aufgenommen? Oder habe ich zufällig einen Slot vergessen, haben sich vielleicht in einzelnen Zeilen Fehler eingeschlichen? Um das rauszufinden muss ich immer alle Fälle durchgehen. Das selbe Script kann ich auch in drei zeilen schreiben indem ich mir die immer wiederkehrende Struktur zunutzen mache:

 
    // go through all 18 INVENTORY_SLOT_XXX Slots, and destroy them  
    int  count;  
    for (count = 0; count < NUM_INVENTORY_SLOTS; count++)  
        DestroyObject(GetItemInSlot(count, oCorpse));  





Nutzt Methoden, die auf den vorhandenen Daten arbeiten, nicht Methoden bevorzugen die andere Ausgangsdaten verwenden, nur weil ihr deren Arbeitsweise besser kennt:

Wenn es eine Methode gibt, die auf den vorhandenen Daten arbeitet sollte diese genutzt werden, egal wie gut ich eine ähnliche Methode kennen mag. Allein dadurch das ich meine Daten auf diese Funktion zubiege schaffe ich neue Fehlerquellen.
Als Beispiel gelten hier die JumpTo-Methoden. Es existieren entsprechende Varianten um zu Objekten oder Locations zu springen. Und meistens sieht man seltsamerweise JumpToLocation() im Einsatz. Selbst wenn das eigentliche Ziel ein Objekt ist.

Man kann davon ausgehen, das die meisten hart kodierten Funktionen ihre Daseinsberechtigung haben. Sofern eine Methode existiert, die meinen Ausgangsdaten entspricht macht es wenig Sinn eine andere zunutzen.

So ist z.B. die JumpToObject Funktion wesentlich sicherer.
Wenn man z.B. mit Wegpunkten arbeitet ist der Umweg über GetLocation() sehr fragwürdig. Springe immer direkt zum Wegpunkt. Nicht zur Location des Wegpunktes. Letzteres ist sehr Fehleranfällig. Steht eine andere Kreatur dort (z.B weil ein anderer Spieler das selbe Script nur kurz zuvor getriggert hat und noch dort steht), oder ist dort ein Placeable, das den Punkt unbegehbar macht, so passiert nichts.

Ein ActionJumpToObject hingegen springt in die Nähe eines Objekts.. Und zwar so nahe wie die Map es erlaubt. Daher sehr viel sicherer.
Auch vom logischen macht es mehr Sinn. Ein NSC begibt sich an einen Wegpunkt. Nicht an die Position an der sich ein Wegpunkt befindet. Sonst hätten die Programmierer von Bioware die Funktion "GetWaypointByTag()" durch eine "GetWayPointLocationByTag()" ersetzt. Aber es reicht eben der Wegpunkt.. und die Location an der er sich befindet ist uninteressant.

--
   ,---\----/ 
   \____\  /    Stefan Vitz aka Stevit     ehem. Serverbetreuung & Scripting 
_______/ \/     projektlos und glücklich. 

[Externer Link - Bitte einloggen oder registrieren]

Carsten

Profil anschauen ]

Gilde/Clan:
City of Arabel

erstellt am 22.07.2004 um 14:22 Uhr   Homepage besuchen    Zitatantwort        #936331

Be still my beating heart
Dieser Thread: [Externer Link - Bitte einloggen oder registrieren] veranlasste mich dazu, ein wenig zum Thema "Heartbeat-Skripte" zu schreiben.
Heartbeats sind ein sehr heikles Thema, denn sie sind als sehr rechenzeit-intensiv verrufen. Dies stimmt, da sie alle sechs Sekunden aufgerufen werden egal ob es notwendig ist oder nicht. Befindet sich also ein Spieler irgendwo im tiefsten Dunkelelfen-Dungeon, wird ein schlecht programmierter Heartbeat immer noch prüfen "Ist gerade Tag oder Nacht" obwohl es keinen Menschen wirklich interessiert da die Sonne 20km weit weg ist. Manche Dinge lassen sich aber nicht anders realisieren. Sinnvoll angewandt können sie durchaus hilfreich sein und Events ersetzen, die nicht in der Engine vorhanden sind. Zum Beispiel gibt es kein Event "Spieler hat Hitpoints verloren" oder "Gesinnung des Spielers hat sich geändert" oder "Es ist Abend". Wenn darauf zuverlässig reagiert werden soll kommt ihr um einen HB kaum herum.
Bei einem HB ist es wichtig zu wissen wo er platziert werden soll - das betrifft zwar alle Skripte, aber bei einem HB ist es besonders wichtig. Es macht wenig Sinn eine Funktion "Krieger Karl wurde verwundet" in ein globales Skript zu packen - denn dann muss bei jeder Verwundung eines NPCs gefragt werden: "Bist Du Krieger Karl? Nein, ok, ignoriere mich". Das braucht zwar nicht viel Rechenzeit, aber es summiert sich eben, speziell auf einem Server auf dem unter Umständen Hunderte von NPCs pro Minute verwundet werden. Es gibt hier keine festen Regeln, denn es ist eine Art Balance-Akt.
Ein Beispiel aus dem Forum dazu: Es wurde gewünscht, dass Ladentüren sich am Abend verriegeln und am Morgen wieder aufsperren. Ein Vorschlag war, den Türen einen HB zu geben der mittels "GetIsNight()" die Tageszeit abfragt, etwa so:

 : 
if (GetIsNight()) 
  SetLocked(TRUE, OBJECT_SELF); 
else 
  SetLocked(FALSE, OBJECT_SELF); 
 : 


Das wird hervorragend funktionieren, hat aber einige Nachteile. Der erste ist: sollte ein Dieb die Ladentür aufsperren, verriegelt sie sich nach spätestens sechs Sekunden wieder. Der zweite ist weniger offensichtlich, da er bei einigen wenigen Läden nicht auffällt. Alle sechs Sekunden fragen ALLE Ladentüren "Haben wir schon Nacht? Nicht? Gut." Hat man aber jetzt ein großes Modul mit einigen Hundert Türen wird nicht mehr viel Anderes laufen. Und selbst wenn nicht, jedes bißchen Rechenzeit dass der Prozessor übrig hat kann für sinnvollere Dinge verwendet werden. Irgendwann ist es nicht mehr sinnvoll jeder Tür einen eigenen HB zu geben, sondern es wird effektiver einen globalen HB zu verwenden der durch alle Türen läuft und sie verriegelt. Wann das ist hängt sehr davon ab wie komplex der Code ist und wie viele Elemente er betrifft. Zweitens muss die Tageszeit ja nicht alle sechs Sekunden geprüft werden. Je nachdem wie schnell die Zeit in eurem Modul läuft reichen alle zwei bis fünf Minuten. Dazu kapselt ihr die Arbeitsroutine im HB mit einem Zähler ab:

 : 
int iZaehler = GetLocalInt(OBJECT_SELF, "iHBCounter")+1; 
if (iZaehler >= 10) 
{ 
  if (GetIsNight()) 
    SetLocked(TRUE, OBJECT_SELF); 
  else 
    SetLocked(FALSE, OBJECT_SELF); 
  iZaehler = 0;	     
}     
SetLocalInt(OBJECT_SELF, "iHBCounter", iZaehler); 
 : 


Ja, der Aufwand zur Prüfung ob der HB durchgeführt werden soll ist hier größer als die eigentliche Arbeitsroutine, aber es ist ja nur ein Beispiel.

Raus aus der Schublade
Oft macht es auch Sinn, sich weniger Gedanken zu machen WIE man etwas erreicht, als vielmehr darum was man eigentlich erreichen will. Im Beispiel oben: das Ziel soll ja sein, dass die Läden nachts geschlossen haben und der Spieler nichts einkaufen kann. Ein Heartbeat das die Türen verriegelt ist nur eine Möglichkeit dies zu tun. Es wäre genausogut möglich:
- dem NPC einen Nacht-Wegpunkt zuzuweisen, von dem er nicht auf das "Laden"-Objekt zugreifen kann weil es in einem anderen Gebiet liegt
- im Gespräch mit dem NPC die Tageszeit abzufragen
- oder einen Trigger vor die Ladentür zu malen der bei Betreten die nächste Tür sucht und je nach Tageszeit auf- oder absperrt
- den Übergang der Türe zu modifizieren dass sie nachts "Hey, wir haben geschlossen" ruft
Besonders die letzten Lösungen haben den Vorteil dass sie nur dann Rechenzeit benötigen, wenn der Spieler tatsächlich etwas mit dem Laden tun will. Beschäftigt sich der Spieler also mit den Drow, bleiben die Läden also unverändert und die Drow können die CPU voll ausnutzen um gemein zu sein.

Ein anderes Beispiel:
Hier [Externer Link - Bitte einloggen oder registrieren] wollte Benjamin, dass der Spieler einen Schrank öffnet, Kleidung herausnimmt und anzieht. Wie ihr an meiner Antwort seht habe ich seine Lösungsversuche total ignoriert und etwas vorgeschlagen das einen vollständig anderen Weg geht: der Schrank öffnet sich selbst und die Kleidung wird direkt per CreateItemOnObject erzeugt. Das Ergebnis ist das Gleiche - der Spieler steht vor dem geöffneten Schrank und hat die Sachen an.

Dies ist besonders dann hilfreich, wenn ihr euch total festgefahren habt. Nehmt euren bisherigen Lösungsansatz, speichert ihn unter einem anderen Namen und versucht etwas Neues. In den meisten Fällen geht der Neuanfang schneller von Statten als der Versuch das Problem zu beheben, denn ihr könnt in der Regel das Wissen des Fehlversuchs wieder verwerten.
Seite: 1
Druckansicht   Thema: [HOWTO]: Hinweise zum besseren Programmier-Stil  
[ ANTWORTEN ]

[RPG]Board Neverwinter Nights NWN Scripting

  


[RPG]Board 1.94.01 wird betrieben von Sebastian "Pandur" Olle.
Programmiert von Andreas "Monti" Bytzek.
Nutzungsbedingungen / Impressum / Haftungsausschluss / Datenschutz
Scriptlaufzeit: 0.28 sec