[Tutorial] Dialoge einfach schreiben mit CHAIN

Jastey

Matron Modderholic
Registriert
16.05.2004
Beiträge
12.946
Beim Schreiben von Dialogen möchte man manchmal auch schnell mal andere NPC etwas einwerfen lassen, während der eigentliche Gesprächspartner zum Beispiel mit dem HC spricht.

Einwürfe von NPCs sind eigentlich ganz einfach. Dazu bietet sich die CHAIN an. Die einfachste CHAIN ist ein Banter zwischen zwei NPC ohne Antwortmöglichkeiten des HC. Dazu findet man auch Beispiele in der Liesmich der WeiDu.

Zu Beginn noch die nötigen Trigger. Da der NPC nur sprechen soll, wenn er in der Gruppe ist, in der Nähe ist und reden kann, brauchen wir also im Prinzip drei Trigger:
-InParty("npcname")
-See("npcname") oder alternativ Detect("npcname")
-und der Trigger, ob der NPC sprechen kann: hierzu hat CamDawg einen Trigger definiert, der alle Fälle ausschließt, die das Sprechen verhindern würden, also stumm, bezaubert, betäubt, ... etcpp.
Dieser ist !StateCheck("mynpc",CD_STATE_NOTVALID)

Um ihn zu verwenden, tut Ihr folgendes in die tp2 Eurer Mod, zu Beginn oder in den ALWAYS Block:

Code:
  /* STATE.IDS patching to ToB - thanks, Cam, if you read it */
  /* adds custom IsValidForPartyDialogue state */
  APPEND ~STATE.IDS~ ~0x80101FEF CD_STATE_NOTVALID~ UNLESS ~CD_STATE_NOTVALID~

Achtung. Imoen ist in allen Spielen eine Extrawurst, wenn Ihr für mehrere Plattformen (BGT/EE) schreibt. Ich verwende hier die Crossplattform-Codes, die Ihr wie gesagt über die cpmvars.tpa einlesen müsst, um sie in der Mod zu verwenden.


1. Einfache CHAIN, zwei NPC sprechen miteinander:

Beispiel: Banter zwischen Ajantis und Rasaad, der von Ajantis ausgeht.

CHAIN
IF WEIGHT #-1 //wenn Ihr dies einfügt, dann wird dieser Banter gegenüber den bereits existierenden vorgezogen. Ajantis würde also als erstes mit Rasaad sprechen, wenn die Engine einen Banter für ihn triggert. Ob Ihr dies immer setzen wollt müsst Ihr entscheiden, ich habe häufig den ersten Banter zwischen zwei NPC damit getaggt aber die folgenden nicht mehr.
~//%BGT_VAR% //Dies ist für Crossplattform-Coding, wenn Ihr für BGT/EET coded, damit das Spiel weiß, dass dieser Banter nicht in BGII laufen soll.
Global("C#AjantisBG1_RasaadBanter","GLOBAL",0) //dies ist die Checkvariable, damit der Banter sich nicht wiederholt.
InParty(Myself) //Ajantis ist in der Gruppe
!StateCheck(Myself,CD_STATE_NOTVALID) //Ajantis kann sprechen
InParty("rasaad") //Rasaad ist in der Gruppe
See("rasaad") //Ajantis kann Rasaad sehen
!StateCheck("rasaad",CD_STATE_NOTVALID) //Rasaad kann sprechen
CombatCounter(0) //es findet gerade kein Kampf statt
!See([ENEMY])~ //Ajantis sieht keinen Feind
THEN ~%AJANTIS_BANTER%~ //das übersetzt sich durch die Crossplattform-Codingweise zu Ajantis' Joined-dlg. Achtung, hierfür müsst Ihr die entsprechenden cpmvars.tpa mit den Definitionen einlesen
rasaad_banter_1 //Name dieses Banters in meinem D-File - wird beim Kompilieren von weidu zu einer Statenummer
@40 //die Texte des Banter sind in einer tra-Datei ausgelagert
DO ~SetGlobal("C#AjantisBG1_RasaadBanter","GLOBAL",1)~ //Variable wird hochgezählt, nun kann der Banter nicht noch einmal triggern. Für den nächsten Banter kann diese Variable einfach verwendet werden!
== ~%RASAAD_BANTER%~ @41 //Rasaad antwortet: auch wieder in Crossplattform-Coding Syntax.
== ~%AJANTIS_BANTER%~ @42
== ~%RASAAD_BANTER%~ @43
= @44 //diese Zeile ist auch von Rasaad
== ~%AJANTIS_BANTER%~ @45
EXIT //die CHAIN wird mit EXIT beendet, wenn es keine Antwortoptionen gibt.

2. Zwei NPC sollen miteinander sprechen, aber der HC soll eine Antwortmöglichkeit haben.

Nehmen wir dafür einfach das Ajantis-Rasaad Beispiel und erweitern es um Antwortmöglichkeiten. Dann sieht es so aus:


CHAIN
IF WEIGHT #-1 //wenn Ihr dies einfügt, dann wird dieser Banter gegenüber den bereits existierenden vorgezogen. Ajantis würde also als erstes mit Rasaad sprechen, wenn die Engine einen Banter für ihn triggert. Ob Ihr dies immer setzen wollt müsst Ihr entscheiden, ich habe häufig den ersten Banter zwischen zwei NPC damit getaggt aber die folgenden nicht mehr.
~//%BGT_VAR% //Dies ist für Crossplattform-Coding, wenn Ihr für BGT/EET coded, damit das Spiel weiß, dass dieser Banter nicht in BGII laufen soll.
Global("C#AjantisBG1_RasaadBanter","GLOBAL",0) //dies ist die Checkvariable, damit der Banter sich nicht wiederholt.
InParty(Myself) //Ajantis ist in der Gruppe
!StateCheck(Myself,CD_STATE_NOTVALID) //Ajantis kann sprechen
InParty("rasaad") //Rasaad ist in der Gruppe
See("rasaad") //Ajantis kann Rasaad sehen
!StateCheck("rasaad",CD_STATE_NOTVALID) //Rasaad kann sprechen
CombatCounter(0) //es findet gerade kein Kampf statt
!See([ENEMY])~ //Ajantis sieht keinen Feind
THEN ~%AJANTIS_BANTER%~ //das übersetzt sich durch die Crossplattform-Codingweise zu Ajantis' Joined-dlg. Achtung, hierfür müsst Ihr die entsprechenden cpmvars.tpa mit den Definitionen einlesen
rasaad_banter_1 //Name dieses Banters in meinem D-File - wird beim Kompilieren von weidu zu einer Statenummer
@40 //die Texte des Banter sind in einer tra-Datei ausgelagert
DO ~SetGlobal("C#AjantisBG1_RasaadBanter","GLOBAL",1)~ //Variable wird hochgezählt, nun kann der Banter nicht noch einmal triggern. Für den nächsten Banter kann diese Variable einfach verwendet werden!
== ~%RASAAD_BANTER%~ @41 //Rasaad antwortet: auch wieder in Crossplattform-Coding Syntax.
== ~%AJANTIS_BANTER%~ @42
== ~%RASAAD_BANTER%~ @43
= @44 //diese Zeile ist auch von Rasaad
== ~%AJANTIS_BANTER%~ @45
END //das END kommt in der CHAIN hierhin, wenn es Antwortoptionen gibt!
++ ~Danke, dass ich bei Eurem Gesprch zuhören durfte!~ EXIT
++ ~Seid Ihr fertig mit reden?~ EXIT

Das ist die Kurzversion: nach der Antwort des HC hört der Dialog auf. Im nächsten Beispiel antworten die NPC darauf noch einmal:
/* Beispiel: Ajantis-Rassad mit Einwurf des HC und nochmal Antwort vom NPC */
CHAIN
IF WEIGHT #-1 //wenn Ihr dies einfügt, dann wird dieser Banter gegenüber den bereits existierenden vorgezogen. Ajantis würde also als erstes mit Rasaad sprechen, wenn die Engine einen Banter für ihn triggert. Ob Ihr dies immer setzen wollt müsst Ihr entscheiden, ich habe häufig den ersten Banter zwischen zwei NPC damit getaggt aber die folgenden nicht mehr.
~//%BGT_VAR% //Dies ist für Crossplattform-Coding, wenn Ihr für BGT/EET coded, damit das Spiel weiß, dass dieser Banter nicht in BGII laufen soll.
Global("C#AjantisBG1_RasaadBanter","GLOBAL",0) //dies ist die Checkvariable, damit der Banter sich nicht wiederholt.
InParty(Myself) //Ajantis ist in der Gruppe
!StateCheck(Myself,CD_STATE_NOTVALID) //Ajantis kann sprechen
InParty("rasaad") //Rasaad ist in der Gruppe
See("rasaad") //Ajantis kann Rasaad sehen
!StateCheck("rasaad",CD_STATE_NOTVALID) //Rasaad kann sprechen
CombatCounter(0) //es findet gerade kein Kampf statt
!See([ENEMY])~ //Ajantis sieht keinen Feind
THEN ~%AJANTIS_BANTER%~ //das übersetzt sich durch die Crossplattform-Codingweise zu Ajantis' Joined-dlg. Achtung, hierfür müsst Ihr die entsprechenden cpmvars.tpa mit den Definitionen einlesen
rasaad_banter_1 //Name dieses Banters in meinem D-File - wird beim Kompilieren von weidu zu einer Statenummer
@40 //die Texte des Banter sind in einer tra-Datei ausgelagert
DO ~SetGlobal("C#AjantisBG1_RasaadBanter","GLOBAL",1)~ //Variable wird hochgezählt, nun kann der Banter nicht noch einmal triggern. Für den nächsten Banter kann diese Variable einfach verwendet werden!
== ~%RASAAD_BANTER%~ @41 //Rasaad antwortet: auch wieder in Crossplattform-Coding Syntax.
== ~%AJANTIS_BANTER%~ @42
== ~%RASAAD_BANTER%~ @43
= @44 //diese Zeile ist auch von Rasaad
== ~%AJANTIS_BANTER%~ @45
END
++ ~Danke, dass ich bei Eurem Gespräch zuhören durfte!~ EXTERN ~%AJANTIS_BANTER%~ antwort1
++ ~Seid Ihr fertig mit reden?~ EXTERN ~%AJANTIS_BANTER%~ antwort2

APPEND ~%AJANTIS_BANTER%~ //wir müssen sagen, welcher dlg die Dialogstates zugeordnet werden
IF ~~ THEN antwort1
SAY ~Gerne.~
IF ~~ THEN EXIT
END

IF ~~ THEN antwort2
SAY ~Ja, jetzt sind wir fertig.~
IF ~~ THEN EXIT
END

END //APPEND //Bei APPEND das END nicht vergessen

Das kann man beliebig weiterspinnen. Sagen wir mal, Rasaad soll am Ende auch nochmal was sagen, nachdem Ajantis auf den Einwurf des Hc reagiert hat, und Imoen kann ihren Mund eh nie halten. Und dafür nehmen wir natürlich wieder eine CHAIN!

/* Beispiel: Ajantis-Rassad mit Einwurf des HC und zurück zu Unterhaltung zwischen den NPCs */
CHAIN
IF WEIGHT #-1 //wenn Ihr dies einfügt, dann wird dieser Banter gegenüber den bereits existierenden vorgezogen. Ajantis würde also als erstes mit Rasaad sprechen, wenn die Engine einen Banter für ihn triggert. Ob Ihr dies immer setzen wollt müsst Ihr entscheiden, ich habe häufig den ersten Banter zwischen zwei NPC damit getaggt aber die folgenden nicht mehr.
~//%BGT_VAR% //Dies ist für Crossplattform-Coding, wenn Ihr für BGT/EET coded, damit das Spiel weiß, dass dieser Banter nicht in BGII laufen soll.
Global("C#AjantisBG1_RasaadBanter","GLOBAL",0) //dies ist die Checkvariable, damit der Banter sich nicht wiederholt.
InParty(Myself) //Ajantis ist in der Gruppe
!StateCheck(Myself,CD_STATE_NOTVALID) //Ajantis kann sprechen
InParty("rasaad") //Rasaad ist in der Gruppe
See("rasaad") //Ajantis kann Rasaad sehen
!StateCheck("rasaad",CD_STATE_NOTVALID) //Rasaad kann sprechen
CombatCounter(0) //es findet gerade kein Kampf statt
!See([ENEMY])~ //Ajantis sieht keinen Feind
THEN ~%AJANTIS_BANTER%~ //das übersetzt sich durch die Crossplattform-Codingweise zu Ajantis' Joined-dlg. Achtung, hierfür müsst Ihr die entsprechenden cpmvars.tpa mit den Definitionen einlesen
rasaad_banter_1 //Name dieses Banters in meinem D-File - wird beim Kompilieren von weidu zu einer Statenummer
@40 //die Texte des Banter sind in einer tra-Datei ausgelagert
DO ~SetGlobal("C#AjantisBG1_RasaadBanter","GLOBAL",1)~ //Variable wird hochgezählt, nun kann der Banter nicht noch einmal triggern. Für den nächsten Banter kann diese Variable einfach verwendet werden!
== ~%RASAAD_BANTER%~ @41 //Rasaad antwortet: auch wieder in Crossplattform-Coding Syntax.
== ~%AJANTIS_BANTER%~ @42
== ~%RASAAD_BANTER%~ @43
= @44 //diese Zeile ist auch von Rasaad
== ~%AJANTIS_BANTER%~ @45
END
++ ~Danke, dass ich bei Eurem Gespräch zuhören durfte!~ EXTERN ~%AJANTIS_BANTER%~ antwort1
++ ~Seid Ihr fertig mit reden?~ EXTERN ~%AJANTIS_BANTER%~ antwort2

APPEND ~%AJANTIS_BANTER%~ //
IF ~~ THEN antwort1
SAY ~Gerne.~
IF ~~ THEN EXIT
END
END //APPEND

CHAIN //hir soll Rasaad jetzt auch was sagen, also nehmen wir CHAIN
IF ~~ THEN ~%AJANTIS_BANTER% antwort2
~Ja, jetzt sind wir fertig.~
== ~%RASAAD_BANTER%~ ~Verzeiht die Verzögerung, <CHARNAME>!~ //Achtung: ob Rasaad sprechen kann müssen wir hier nicht überprüfen, da das oben bereits erfolgt ist.
== ~%IMOEN_BANTER%~//Imoen kommt jetzt neu dazu, da müssen wir überprüfen, ob sie sprechen kann
IF ~InParty("%IMOEN_DV%") //Imoen ist in der Gruppe
See("%IMOEN_DV%") //Der neue NPC kann Imoen sehen
!StateCheck("%IMOEN_DV%",CD_STATE_NOTVALID) //Imoen kann sprechen
THEN ~Manchmal bist du echt pampig, <CHARNAME>!~
EXIT


3. Weitere Möglichkeiten für CHAIN: Gespräch mehrerer NPC und dem HC

Im letzten Beispiel wurde ja schon deutlich, welche Möglichkeiten es gibt. NPCs reden miteinander, Gruppen-NPCs machen einen Einwurf, der HC darf auch was sagen und dann reden NPCs wieder miteinander. Die Elemente, die ich dabei verwende, sind APPEND und CHAIN - und zwar ordne ich sie im d-File so an, dass man den Dialog gut lesen kann. Dem Installer ist es egal, ob er beim Kompilieren hin- und herhüpft. Mir beim Lesen ist es das nicht, und unübersichtliche d-Files machen einem das Leben beim Debuggen und Weiterschreiben schwer.

Zum Abschluss noch folgendes Beispiel: Ein neuer NPC (z.B. Euer!), der sich der Gruppe vorstellt und ein Gruppenmitglied soll etwas dazu sagen, bevor der HC seine Antworten geben kann.


BEGIN xxmyNPC //ganz zu Beginn einer neuen dlg muss diese über BEGIN definiert werden.

/* jetzt kommt der Begrüßungsdialog mit Einwurf von Imoen: */

CHAIN
IF ~See(Player1) Global("xxHelloThere","GLOBAL",0)~ THEN xxmyNPC hallo
~Seid gegrüßt, Fremde!~
DO ~SetGlobal("xxHelloThere","GLOBAL",1) //beim nächsten Ansprechen triggert dieser Dialog nicht mehr
== %IMOEN_JOINED% IF ~InParty("%IMOEN_DV%") //Imoen ist in der Gruppe
See("%IMOEN_DV%") //Der neue NPC kann Imoen sehen
!StateCheck("%IMOEN_DV%",CD_STATE_NOTVALID) //Imoen kann sprechen
THEN ~Also, ich und <CHARNAME> sind uns nicht fremd. Wir kennen uns schon ganz lange!~
== xxmyNPC IF ~InParty("%IMOEN_DV%") See("%IMOEN_DV%")
!StateCheck("%IMOEN_DV%",CD_STATE_NOTVALID) //der neue NPC antwortet - nur, wenn Imoen ihren Satz gesagt hat!
THEN ~Naja, das sagt man halt so.~
END
++ ~Seid gegrüßt. Wer seid Ihr?~ EXTERN xxmyNPC hallo_01
++ ~Ich möchte nicht mit Euch sprechen.~ EXTERN xxmyNPC hallo_02

CHAIN
IF ~~ THEN xxmyNPC hallo_01
~Oh, Ihr kennt mich nicht? Dann müste ich mich wohl mal vorstellen.~
== %IMOEN_JOINED% IF ~InParty("%IMOEN_DV%") See("%IMOEN_DV%")
!StateCheck("%IMOEN_DV%",CD_STATE_NOTVALID) //Imoens Anwesenheit muss auch hier überprüft werden
THEN ~Puh, das kann ja eine Weile dauern.~
== xxmyNPC ~Ich bin jeden Tag hier, falls Ihr Fragen habt - ich stehe da drüben.~ //Diese Zeile ist unabhängig von Imoens Einwurf!
EXIT

APPEND xxmyNPC
IF ~~ THEN hallo_02
SAY ~Gut, dann nicht.~
IF ~~ THEN EXIT
END

END //APPEND

4. Weitere Möglichkeiten für CHAIN: NPC soll sich auf verschiedene Begebenheiten beziehen

CHAIN bietet aber noch mehr Möglichkeiten. Z.B ist es eine sehr einfache Art, den HC zu ganz verschiedenen Begebenheiten in einem Gespräch Bezug nehmen zu lassen. Nehmen wir mal das Beispiel, ob ein bestimmtes Event bereits geschehen ist oder nicht. Das fragen wir mit einer selbstgesetzten Variable "Global("xxWichtigesEventPassiert","GLOBAL",1)" ab. Die Unterhaltung mit dem NPC sieht dann so aus:

CHAIN
IF ~See(Player1) Global("xxHelloThere","GLOBAL",0)~ THEN xxmyNPC hallo //Das "See(Player1)" stellt sicher, dass der HC beim Gespräch dabei ist. Führt natürlich dazu, dass der NPC einen anderen Begrüßungsdialog braucht, wenn der Hc nicht in der Nähe ist!
~Seid gegrüßt, Fremde!~
DO ~SetGlobal("xxHelloThere","GLOBAL",1) //beim nächsten Ansprechen triggert dieser Dialog nicht mehr
== %IMOEN_JOINED% IF ~InParty("%IMOEN_DV%") //Imoen ist in der Gruppe
See("%IMOEN_DV%") //Der neue NPC kann Imoen sehen
!StateCheck("%IMOEN_DV%",CD_STATE_NOTVALID) //Imoen kann sprechen
THEN ~Also, ich und <CHARNAME> sind uns nicht fremd. Wir kennen uns schon ganz lange!~
== xxmyNPC IF ~InParty("%IMOEN_DV%") See("%IMOEN_DV%")
!StateCheck("%IMOEN_DV%",CD_STATE_NOTVALID) //der neue NPC antwortet - nur, wenn Imoen ihren Satz gesagt hat!
THEN ~Naja, das sagt man halt so.~
== xxmyNPC IF ~Global("xxWichtigesEventPassiert","GLOBAL",1)~ THEN ~Ist das nicht *schrecklich*, was da vorhin geschehen ist? Ich bin noch ganz benommen!~ //diese Zeile kommt nur, wenn die Variable entsprechend gesetzt wurde.
END
++ ~Seid gegrüßt. Wer seid Ihr?~ EXTERN xxmyNPC hallo_01
++ ~Ich möchte nicht mit Euch sprechen.~ EXTERN xxmyNPC hallo_02
+ ~Global("xxWichtigesEventPassiert","GLOBAL",1)~ + ~Ja, ich fand das auch ziemlich übel.~ EXTERN xxmyNPC hallo_03 //Diese Antwortoption erscheint nur, wenn das Event geschehen ist!

Dies kann in allen Dialogen für alle möglichen Trigger / Begebenheiten / Romanzenstatus / Rasse/Klasse/Kit/Charismalevel der HC verwendet werden. Das ist wirklich Klasse, da man dadurch in den Dialogen auf viele vorher getane oder gesagte Dinge (sofern man sie detektieren kann) Bezug nehmen kann, was die Dialoge sehr viel immersiver macht - und es ist kein bisschen aufwändig, das zu programmieren!

(Dieses Tutorial soll die Prinzipien verdeutlichen. Es könnten sich also einzelne Syntaxfehler darin befinden, die erst beim Kompilieren auffallen. C# ist mein persönliches Präfix. Bitte reserviert Euch Euer eigenes in der Präfixliste.)
 
Zuletzt bearbeitet:

Acifer

Senior Member
Registriert
27.04.2019
Beiträge
2.190
Etwas, das ich die ganze Zeit schon sagen wollte: Das ist ein ganz tolles Tutorial!

Mein Problem bisher war zum Beispiel, dass ich nicht genau wusste, wie ich in einer bestehenden Chain Antwortoptionen des Spielers einbringen konnte und die Unterhaltung zwischen den NPCs dennoch weiterläuft.
Sodass ich nach der ersten Antwort des HC dann wieder per 100x EXTERN im PingPong-Verfahren zwischen den NPCs hin- und hergesprungen bin.
Dein Ansatz, nach der ersten Antwort des Spielers einfach eine neue Chain zu starten, kam mir gar nicht in den Sinn. :hae::cool::up:!
So liest sich der Dialog natürlich dann absolut flüssig und man weiß auch nach einem halben Jahr noch, wo was im D-file weitergeht.
Danke für Deine wertvollen Erläuterungen. :)
 

Jastey

Matron Modderholic
Registriert
16.05.2004
Beiträge
12.946
CamDawg hat eine schöne Ergänzung zum raffinierten Dialoge schreiben: COPY_TRANS für eigene Dialogstates nutzen, um die Antwortoptionen nicht doppelt hinschreiben zu müssen. Das Prinzip ist ja klar, aber das so zu nutzen war ich auch noch nicht drauf gekommen. Link zum Originalpost. Sein Beispiel:
Code:
BEGIN CDTEST

  IF ~RandomNum(2,1)~ Intro1 SAY ~Greetings! I have an incredible offer for you.~
    IF ~RandomNum(3,1)~ THEN REPLY ~No thanks.~ EXIT
    IF ~RandomNum(3,1)~ THEN REPLY ~Yes.~ GOTO QuestExpo
    IF ~RandomNum(3,2)~ THEN REPLY ~Short answer: no. Long answer: noooooooooo.~ EXIT
    IF ~RandomNum(3,2)~ THEN REPLY ~Yes, please.~ GOTO QuestExpo
    IF ~RandomNum(3,3)~ THEN REPLY ~Ah, hell naw.~ EXIT
    IF ~RandomNum(3,3)~ THEN REPLY ~Ooh, tell me, tell me, tell me!~ GOTO QuestExpo
  END

  IF ~RandomNum(2,2)~ Intro2 SAY ~Hello freinds! Would you like to hear about my exciting quest?~
    COPY_TRANS CDTEST Intro1
  END

  IF ~~ QuestExpo SAY ~(exciting quest exposition here)~ 
    IF ~~ THEN EXIT
  END
Das eigentliche Beispiel ging um das Randomisieren von Antwortoptionen und Begrüßungszeilen, aber mir geht es darum, dass durch das COPY_TRANS die ersten beiden States dieselben Antwortoptionen nutzen, ohne, dass man sie im eigenen Code doppelt hinschreiben muss.

EDIT: Wenn man dies in anderem Zusammenhang verwenden möchte, z.B. wie ich hier in Dialogstates, die über APPEND angehängt werden, dann muss man stattdessen COPY_TRANS_LATE verwenden, damit die Engine den Statenamen schon auflösen kann.
 
Zuletzt bearbeitet:
Oben