Clonk-Center Titelbild

Texte / Effekte (CNDG)




Was sind Effekte?

Effekte sind primär ein Scriptgerüst, womit sich vieles komfortabel erledigen lässt. Effekte können als Timer fungieren, wenn dies über DefCore oder ActMap nicht möglich ist. Außerdem kann jeder Effekt auf einen anderen Effekt reagieren und sich in verschiedenen Situationen zu Wort melden. Sie sind jedoch zuerst wirklich nur scriptbezogen und - was die meisten unter einem "Effekt" verstehen - nicht für Grafisches zuständig. Ein Effekt kann jedoch trotzdem grafische Aktionen ausführen, z.B. Partikel verschleudern.
Effekte können also als Timer verwendet werden, um zum Beispiel jede Sekunde die gleiche Aktion auszuführen, z.B. die Lebensenergie des Clonks auffüllen - schon hat man eine Energieregeneration an den Clonk gehängt. Ein Effekt meldet sich aber auch, wenn das Objekt Schaden erleidet, zum Beispiel über DoEnergy() oder DoDamage(). Egal ob Energie gefüllt oder Schaden verursacht wurde, es wird an jeden Effekt eine Meldung ausgegeben, und jeder kann darauf reagieren. Jeder Effekt für sich kann den Schadenswert beeinflussen, ob nach oben oder nach unten, komplett abblocken oder ganz eigene Dinge ausführen. Und zu guter Letzt meldet sich ein Effekt, wenn das Objekt einen zusätzlichen Effekt bekommen soll. Beispielsweise hat man seinem Clonk einen "Unbrennbar-Zauber" angehängt, und der Clonk läuft geradewegs durchs Feuer. Die Engine gibt eine Meldung aus, dass sie dem Clonk auch gerne eine Flamme anhängen will - nun kann der Effekt das Feuer einfach verweigern, und der Clonk kann gemütlich durchs Feuer gehen.


Grundlagen

Wofür Effekte gebraucht werden können, wissen wir jetzt. Aber wie hängen wir einen Objekt überhaupt einen Effekt an?
Hier hilft AddEffect. Mit ganzen 10 Parametern nicht ganz einsteigerfreundlich, aber es gibt Entwarnung: Die letzten 5 braucht man oftmals sowieso nicht, also wird das Ganze schon einfacher. Die gesamte Funktion sieht so aus:

int AddEffect(string szEffectName, object pTarget, int iPrio, int iTimerIntervall, object pCmdTarget)
szEffectName: Der Name des Effekts als String, zB "Unverwundbarkeit".
pTarget: Das Objekt, welches den Effekt bekommen soll. Meistens werdet ihr einfach this benötigen. Achtung: Wenn pTarget 0 ist, wird der Effekt global ohne Objektkontext erzeugt.
iPrio: Effektpriorität. Je höher, desto "wichtiger" ist der Effekt. Zu Beginn muss man sich darum nicht viele Gedanken machen, allerdings darf dieser Wert nicht 0 sein.
iTimerIntervall: Wird für Timer benötigt. Gibt den Zeitabstand zwischen Timer-Aufrufen an.
pCmdTarget: Das Callback-Objekt. Grob gesagt das Objekt, wo die Engine die Effekt-Funktionen findet. Wenn sie im selben Script stehen, reicht auch hier einfach this.

Natürlich lassen sich auch globale Effekte erzeugen. Dies ist möglich, wenn pTarget und pCmdTarget 0 sind. Dann ist der Effekt global und die Funktionen müssen im Szenarioscript als global definiert sein.

Anstatt nun einen komplett neuen Effekt zu schreiben, versuchen wir es mit einem bereits existierenden Effekt. Jeder kennt ihn: Das Feuer. Feuer ist ebenfalls ein Effekt, der in der Engine definiert wurde. Nun können wir damit einfach Clonks anzünden: Wir führen in einem Clonk die Scriptzeile
AddEffect("Fire", this, 100, 1, this);
aus. Sie fügt dem Clonk den Effekt "Fire" hinzu (so heißt das Engine-Feuer) und gibt dem Effekt die Prorität 100, also dieselbe wie dem Engine-Feuer. Und siehe da: Der Clonk brennt und verliert langsam an Lebensenergie und kann auch andere Objekte anzünden!

Damit bei jedem Funktionsaufruf sichergestellt ist, dass der richtige Effekt und das richtige Objekt gemeint sind, werden an jede Funktion das Zielobjekt als 1. und die Effektnummer als 2. Parameter übergeben. Das ist vor allem dann nützlich, wenn man zusätzliche Effektdaten abfragen will, denn diese Parameter braucht man dafür, um auch die richtigen Daten zurückzubekommen.
Halt, Effektdaten? Ja, Effekte haben einen eigenen Speicher. Man kann, statt lokale Variabeln für einen Effekt zu nutzen, diese im Effekt selbst speichern. Dazu aber später mehr.


Effekte als Timer

Nun aber zu unserem ersten eigenen Effekt. Wir beginnen möglichst einfach: mit einem Timer. Er soll nichts anderes tun, als regelmäßig eine Funktion aufzurufen. Wer nun einwenden möchte, man könne doch Schedule() oder ScheduleCall() benutzen, dem sei gesagt: Das sind ebenfalls Effekte, nämlich IntSchedule und IntScheduleCall, sie funktionieren auf dieselbe Weise.
Sagen wir, wir wollen uns ein Objekt bauen, das alle 50 Frames kurz nach oben springt. Also fügen wir ihm einen Effekt hinzu mit dem Intervall 50 und Priorität 1, welche vorerst nicht interessiert.
Die Funktion, die dann immer wieder aufgerufen wird, heißt Fx*Timer, wobei * der Effektname ist. Das Engine-Feuer ruft also zB immer wieder FxFireTimer() auf. Unseren Effekt nennen wir Flummi, also heißt die Funktion FxFlummiTimer().
Effektfunktionen sollten protected sein, da sie von der Engine aufgerufen werden.

/* Seltsamer Stein */

#strict 2

protected func Initialize() {
AddEffect("Flummi", this, 1, 50, this);
}

protected func FxFlummiTimer() {
//Boing!
SetYDir(-50);
}


Und schon haben wir ein Objekt, das auf und ab springt.

Aber auch mit einem simplen Timer ist noch weit mehr möglich. Die Fx*Timer()-Funktion bekommt nicht nur wie alle anderen Funktionen das Objekt und die Effektnummer, sondern auch die Zeit, wie lange der Effekt schon läuft. So sind viele grafische Spielereien um ein Vielfaches einfacher zu bewältigen.
Angenommen, wir haben einen tollen Unverwundbarkeitseffekt. Natürlich soll man das dem Clonk auch ansehen! Meist kreist dann um den Clonk etwas herum. Das wollen wir auch haben - einen Partikel, der um den Clonk herumkreist! Dazu nutzen wir Sin() und Cos() und als Grad-Angabe die Effektzeit. und schon bewegt sich der Partikel!

#strict 2
#appendto CLNK

protected func Initialize() {
//Unverwundbar!
AddEffect("Unverwundbarkeit", this, 200, 1, this);
//Den Rest vom Clonk natürlich auch...
return _inherited(...);
}

protected func FxUnverwundbarkeitTimer(object pTarget, int iEffectNumber, int iTime) {
//Partikel! Per Sin und Cos kreisförmig. Natürlich in Clonk-Farbe.
CreateParticle("PSpark", Sin(iTime, 20), Cos(iTime, 20), 0, 0, 50, GetColorDw(pTarget), pTarget);
}


Der Partikel schwebt nun um unseren Clonk herum. Mit nur so wenig Script haben wir einen eigentlich brauchbaren kleinen grafischen Effekt erzeugt! Der Partikel bewegt sich aber noch recht langsam. Das ändern wird durch z.B. 4*iTime oder Ähnlichem.
Natürlich macht der Effekt den Clonk nicht unverwundbar, das kommt später. Aber der grafische Grundstein ist gelegt. Jedoch: Der Effekt bleibt nun für immer. Das ist natürlich nicht in unserem Sinne, wir wollen den Effekt irgendwann auch loswerden, sonst bekommt keiner den Clonk tot. Das erreichen wir auf 2 Arten. Die erste ist RemoveEffect. Hier können wir einfach RemoveEffect("Unverwundbarkeit", this) angeben, und der Effekt ist gebannt. Das Objekt (hier this) darf allerdings auch bei lokalen Aufrufen nicht 0 sein. Die zweite Möglichkeit ist eine Besonderheit der Fx*Timer-Funktion: Wenn diese -1 zurückgibt, wird der Effekt automatisch entfernt. Die Sache gestaltet sich also einfach: Sobald iTime einen gewissen Wert überschreitet, gibt die Funktion -1 zurück, und unser Clonk ist vom Effekt befreit.

#strict 2
#appendto CLNK

protected func Initialize() {
//Unverwundbar!
AddEffect("Unverwundbarkeit", this, 200, 1, this);
//Den Rest vom Clonk natürlich auch...
return _inherited(...);
}

protected func FxUnverwundbarkeitTimer(object pTarget, int iEffectNumber, int iTime) {
//Partikel! Per Sin und Cos kreisförmig. Natürlich in Clonk-Farbe.
CreateParticle("PSpark", Sin(4*iTime, 20), Cos(4*iTime, 20), 0, 0, 40, GetColorDw(pTarget), pTarget);
//Und noch einer!
CreateParticle("PSpark", -Sin(4*iTime, 20), -Cos(4*iTime, 20), 0, 0, 40, GetColorDw(pTarget), pTarget);
//300 Frames - und dann ist alles vorbei.
if (iTime > 300) return -1;
}


Und er sieht toll aus.


Effekte und Schaden

Unser Clonk ist glücklich mit seinem neuen, wundervoll aussehenden Unverwundbarkeitseffekt. Leider hat er noch keinen Sinn, da er erhaltenen Schaden immer noch durchlässt. So nützt er uns erstmal nichts. Aber wie bearbeitet man den Schaden?
Hier brauchen wir Fx*Damage. Diese Funktion wird für jeden Effekt aufgerufen, sobald das Objekt Schaden erhält. In diesem kann man alles Mögliche anstellen, zum Beispiel den Schaden für einen Schildzauber halbieren, bei einem geschwächten Clonk verdoppeln oder Ähnliches. Wir wollen hier aber nur eines: Gar nichts hindurchlassen, schließlich soll der Clonk unverwundbar sein.
Der Fx*Damage-Aufruf bekommt als dritten Parameter den verursachten Schaden (iDmgEngy) und als 4. Parameter den Schadenstyp (iCause, also zB Feuer, Explosion, Sauerstoffmangel, Faustkampf etc.). Alle Schadenstypen findet man in der Effekte-Dokumentation in der Online-Doku, und man kann sogar selbst welche festlegen, aber erstmal wollen wir wirklich alles abblocken, also interessiert uns der vorerst nicht.
iDmgEngy ist der erhaltene Schaden. Allerdings bekommt man immer den "echten" Energiewert des Objekts. Bei Clonks ist das maximal 100.000. Dabei ist dieser Wert immer das Tausendfache von zB Scriptaufrufen, einfach aus dem Grund, dass genauere Abstufungen möglich sind. Wenn man dem Clonk also per DoEnergy(-1) Leben abzieht, wird an den Effekt ein Schadenswert von -1.000 übergeben. Positive Werte sind immer hinzugefügte Energie, negative Werte werden abgezogen.
Beim Fx*Damage-Aufruf bestimmt der Rückgabewert, wieviel Schaden der Effekt tatsächlich erlaubt.

Wir nehmen den obigen Clonk-Script und erweitern ihn um die folgenden Zeilen:

//Wir brauchen keine Parameter. Ursache und Schadensmenge sind uns egal, wir blocken einfach alles ab.
protected func FxUnverwundbarkeitDamage() {
//Hier kommt nichts durch!
return 0;
}


Nun können wir den Clonk mit Objekten bewerfen, mit explodierenden Ölfässern und Teraflints traktieren oder gegen den Clonk-Rambo von nebenan kämpfen lassen: In allen Fällen meldet sich der Effekt mit einem "Halt! Ich lass hier keinen Schaden durch!" - die Lebensleiste des Clonks bleibt randvoll. Nach 300 Frames sollte man dann aber damit aufhören - dann entfernt sich der Effekt (wie oben gescriptet), und der Clonk ist wieder vollkommen verwundbar. Denn die Fx*Damage-Aufrufe werden nur dann getätigt, wenn momentan auch wirklich dieser Effekt am Objekt hängt! Das macht das Arbeiten mit Effekten so einfach.

Das krasse Gegenteil klappt natürlich auch: Der Clonk hat gleich zu Beginn einen Schwäche-Anfall und bekommt so stets das Fünffache an Schaden! Mit ihm muss man wirklich umgehen wie mit einem rohen Ei:


#strict 2
#appendto CLNK

protected func Initialize() {
//Autsch. Das wird wehtun.
AddEffect("SchwaecheAnfall", this, 200, 0, this);
return _inherited(...);
}

protected func FxSchwaecheAnfallDamage(object pTarget, int iEffectNumber, int iDmgEngy) {
//Schmerz lass nach.
return 5*iDmgEngy;
}


Bereits ein Objekt aus geringer Höhe bedeutet für den Clonk den sicheren Tod. Ein Feuerstein ist absolut tödlich. Auch durch Feuer, und selbst durchs Ertrinken oder im Faustkampf stirbt der Clonk rasant. Solche Spielereien sind mit Effekten problemlos machbar - dies wäre zB Grundlage für einen Schmerzensfluch, den man dem Gegner wohl sehr gerne anhängen wird.


Andere Effekte

Unser Unverwundbarkeits-Effekt ist nicht allein auf der Welt! Nein, auch andere Effekte wollen unserem Clonk an den Kragen oder ihm helfen, vor allem in Magieszenarien sind sie allgegenwärtig. Wer mit dem Unverwundbarkeitseffekt etwas herumgespielt hat, wird schnell herausfinden, dass der Clonk zwar wirklich resistent gegen jeden Schaden ist - angezündet werden kann er aber trotzdem. Sobald der Effekt dann stoppt, wird der Clonk ein Opfer der Flammen.
Wie wir vorhin schon festgestellt haben, ist auch das Feuer der Engine nur ein Effekt. Glücklicherweise muss jeder Effekt alle "wichtigeren" Effekte um Erlaubnis fragen, ob er hinzugefügt werden darf. Hier kommt die Prorität ins Spiel: Feuer hat die Priorität 100, also werden alle Effekte, die am zu entzündenden Objekt hängen und eine Priorität >= 100 haben, gefragt, was sie vom neuen Effekt halten. Unser Unverwundbarkeits-Effekt (Priorität 200) wird ebenfalls befragt.
Dabei können die Effekte 2 (genau genommen 3) Statements (über den Rückgabewert) abgeben:
-1: Effekt verweigern.
-2: Effekt übernehmen
-3: Effekt übernehmen und alle höheren Effekte temporär entfernen
Wichtig ist zuerst mal -1. -2 mag man später mal brauchen, -3 so gut wie nie, mir fiele zumindest gerade kein solcher wirklich sinnvoller Fall ein.

Was soll unser Effekt also tun? Er soll überprüfen, was das für ein neuer Effekt ist und - wenn er unserem Clonk Böses antun will - den Effekt einfach verweigern.
Der Aufruf Fx*Effect erhält dafür als 1. Parameter den Namen des neuen Effekts. Das Zielobjekt und der befragte Effekt rutschen dafür eine Stelle nach hinten. Wenn also nun der neue Effektname "Fire" ist, soll unsere Funktion -1 zurückgeben. Und um gleich noch etwas weiter auszuholen: Effekte haben gewisse Namensregeln, so sollten alle negativen magischen Effekte "NSpell" (N: negativ, Spell: Zauber) im Namen haben. Die mögen wir natürlich auch nicht. Um den Namen des neuen Effekts darauf abzutesten, benötigen wir WildcardMatch.
An unseren Script hängen wir nun:

//Bestimmte Effekte abblocken.
protected func FxUnverwundbarkeitEffect(string szNewEffectName) {
if (szNewEffectName == "Fire" || WildcardMatch(szNewEffectName, "*NSpell*"))
return -1;
}


Wenn wir nun versuchen, unseren Clonk durch einen Waldbrand spazieren zu lassen, wird das problemlos funktionieren. Unseren Clonk lässt das jetzt kalt - solange der Effekt nicht ausgeht...


Start und Stop
Unser Unverwundbarkeitseffekt steht nun und ist vollständig verwendbar. Nun gibt es aber auch Effekte, bei denen Dinge direkt zu Beginn getätigt werden müssen, oder sobald der Effekt entfernt wird. Auch hierfür gibt es Funktionen: Fx*Start und Fx*Stop.
Im Beispiel wollen wir unserem Clonk einen Unsichtbarkeitszauber anhängen. Dabei wird er zu Beginn unsichtbar gemacht und bei Entfernung des Effekts wieder sichtbar gemacht.
Zuerst benötigen wir die Anweisung, welche Spieler den Clonk noch sehen dürfen. Das ist einerseits der Spieler selbst, und zweitens der "Gott", also wenn ein Spieler zB ausgeschieden ist und im Zuschauermodus die "Freie Sicht" aktiviert hat. Verbündete dürfen ihn natürlich auch noch sehen - nicht dass diese ihn aus Versehen abschießen. Wir bekommen das Ganze per SetVisibility hin.
Außerdem wollen wir den Clonk für eben diese Personen nicht komplett sichtbar machen, sondern halbtransparent, was mit SetClrModulation klappt.

Leider haben Start und Stop einen Haken. Wie wir wissen, können Effekte andere Effekte kurzzeitig entfernen, indem sie für einen neuen Effekt -3 zurückgeben. Die Start- und Stop-Aufrufe werden dann trotzdem getätigt, obwohl der Effekt da bleibt! Man muss also darauf achten, keine unnützen oder gar fehlerhaften Aufrufe für einen bereits laufenden Effekt ausführen zu lassen. Hierzu bekommt Fx*Start als 3. Parameter iTemp, und Fx*Stop als 4. Parameter fTemp. Sobald diese wahr sind, sollte man den Aufruf abbrechen.

#strict 2
#appendto CLNK

protected func Initialize() {
AddEffect("Invisible", this, 100, 400, this);
return _inherited(...);
}

protected func FxInvisibleStart(object pTarget, int iEffectNumber, int iTemp) {
//Temporär? Abbruch
if (iTemp) return;
//Na, wer sieht ihn denn noch?
SetVisibility(VIS_Owner | VIS_God | VIS_Allies);
//Halbtransparent für Unsichtbarkeits-Flair
SetClrModulation(RGBa(255,255,255,128));
}

protected func FxInvisibleStop(object pTarget, int iEffectNumber, int iReason, bool fTemp) {
//Temporär? Abbruch
if (fTemp) return;
//Für alle sichtbar
SetVisibility(VIS_All);
SetClrModulation(RGBa(255,255,255));
}


Hier wurde noch ein gewisser Kniff angewendet: Der Effekt hat einen Timer von 400 bekommen, müsste also alle 400 Frames die Funktion FxInvisibleTimer() aufrufen. Diese gibt es aber gar nicht! In diesem Fall entfernt sich der Effekt nach 400 Frames, ruft FxInvisibleStop() auf und der Clonk wird dadurch wieder sichtbar.


Effekte in Funktionen einbeziehen

Es ist natürlich möglich, irgendetwas mit Effekten anzustellen, ohne gerade in einem Fx*...-Aufruf zu sein. Dazu gibt es verschiedende Befehle, am meisten wird man wohl GetEffect benötigen. Damit kann man zum Beispiel abfragen, ob ein Objekt momentan einen bestimmten Effekt besitzt. Hiermit noch einmal zurück zu unserem Unverwundbarkeitseffekt: Der Clonk ist zwar bereits resistent gegen Schaden und einige andere Effekte, allerdings wird der Clonk immer noch von Objekten getroffen. Das ist nervig, wenn man trotz Unverwundbarkeit in den Abgrund geschubst werden kann.
Die Engine bietet dafür eine Lösung: QueryCatchBlow! Diese Funktion wird in einem Objekt aufgerufen, bevor es von einen anderen getroffen wird. Wenn diese Funktion dann einen Wert != 0 zurückgibt, wird dieser Effekt abgefangen. So kann man selbst zB die gefürchteten Granitschläge aus Magieszens umgehen - sie fliegen am Clonk vorbei.

Um das zu realisieren, geben wir bei QueryCatchBlow() einfach GetEffect() || _inherited(...) zurück. Das ist eine gewisse Raffinesse, die den script verkürzt und dank #strict 2 besonders leichtfällt. Denn hat der Clonk den Effekt, wird die überladene Funktion ignoriert, da der Rückgabewert sowieso wahr ist. Existiert der Effekt nicht, wird _inherited(...) geprüft, welches ja auch noch wahr sein kann. Ist das falsch, wird false zurückgegeben, der Clonk wird getroffen.
Also im Clonkscript:

protected func QueryCatchBlow() {
return GetEffect("Unverwundbarkeit", this) || _inherited(...);
}


Zum Vergleich, der "ausführliche" Script:

protected func QueryCatchBlow() {
if (GetEffect("Unverwundbarkeit", this))
return true;
if (_inherited(...))
return true;
return false;
}



Effekte als Datenspeicher

Oft ist es nötig, einem Effekt bestimmte Informationen zu geben. Viele speichert er selbst - das Zielobjekt, die Effektnummer, die Laufzeit, etc. Jeder Effekt kann beliebig viele Zusatzinfos aufnehmen. Dazu steht sozusagen ein Array zur Verfügung, welches man mit EffectVar abfragen kann. Dabei gibt der erste Parameter den Array-Index an, der 2. ist das Objekt und der dritte die Effektnummer. Alles das benötigt man, um auch wirklich verwechslungsfrei die richtigen Daten zu bekommen. Das Objekt und die Effektnummer werden glücklicherweise an jeden Fx*...-Aufruf übergeben - das macht das Ganze schon recht einfach, tatsächlich wird man meistens EffectVar(x, pTarget, iEffectNumber) benutzen, wobei das x für eine beliebige Zahl steht.

Nun wollen wir eine hinterhältige Waffe bauen. Wir wollen ein Objekt, das man durch Doppelgraben aktivieren kann. Wenn es ein Gegner einsammelt - BOOM! Um das Ganze noch gemeiner zu machen, machen wir es (fast) unsichtbar und schützen den eigenen Clonk dagegen. Wäre ja dämlich, sich selbst in die Luft zu jagen.
Wir erinnern uns noch daran, dass AddEffect 10 Parameter nimmt. Die letzten 4 sind Zusatzinformationen, die zum Beispiel an Fx*Start weitergegeben werden! Somit können wir den Clonk einfach an den Effekt weitergeben, der den Besitzer des Clonks speichert. So erreichen wir, dass man nicht die eigene Falle aufsammeln kann - das müssen wir dann noch bei RejectCollect() überprüfen.

/*-- Hinterhältige Falle --*/

#strict 2

//Wird bei Doppelgraben aufgerufen
protected func Activate(object pClonk) {
//Effekt!
AddEffect("Falle", this, 1, 0, this, 0, pClonk);
return 1;
}

protected func FxFalleStart(object pTarget, int iEffectNumber, int iTemp, var1) {
if (iTemp) return;
//var1 ist der Clonk, der das Ding aktiviert hat! Dessen Besitzer packen wir ins Array:
EffectVar(0, pTarget, iEffectNumber) = GetOwner(var1);
//Und schlecht sichtbar:
SetClrModulation(RGBa(255, 255, 255, 180));
}

//Beim Einsammeln werden wir den Effekt direkt entfernen. Boom!
protected func FxFalleStop(object pTarget, int iEffectNumber, int iReason, bool fTemp) {
if (fTemp) return;
Explode(30);
}

//RejectEntrance wird aufgerufen, wenn jemand das Objekt einsammeln will.
//Bei Rückgabe "true" wird das Objekt nicht eingesammelt.
protected func RejectEntrance(object pInto) {
//Objekt hat noch gar keinen Fallen-Effekt: Okay! Clonk gehört dem Aktivierenden: Verweigern!
return GetEffect("Falle", this) && GetOwner(pInto) == EffectVar(0, this, GetEffect("Falle", this));
}

//Objekt wurde eingesammelt: Hochjagen!
protected func Entrance() {
RemoveEffect("Falle", this);
}


Man hätte das Ganze auch ohne Effekt lösen können. Eine lokale Variable, ob es schon aktiviert ist, und wem es gehört. Das würde auch klappen. Allerdings hat der Effekt einen Vorteil: Möchte man zB einen Fallendetektor bauen, der alle Fallen in einem gewissen Umkreis hochjagt, können wir einfach bei jedem Objekt überprüfen, ob es den Effekt hat - das geht recht leicht. Bei der anderen Variante müsste man mit LocalN() größere Gerüste bauen. Die Effektprüfung käme da mit einer Anweisung aus. Zum Vergleich, die Scriptpassage bei den verschiedenen Varianten:

//Effekt:
for (var obj in FindObjects(Find_Distance(100)))
RemoveEffect("Falle", obj);

//Lokale Variabeln:
for (var obj in FindObjects(Find_Distance(100), Find_ID(FALL)))
if (LocalN("Aktiv", obj))
Explode(30, obj)


Gut, hier ist der Unterschied noch klein. Möchten wir aber zur Explosion weitere Anweisungen hinzufügen, wird die Liste nach der if-Klammer durchaus länger. Flammen verschleudern, Partikel, Sound, was man eben haben will. Das könnten wir beim Effekt alles einfach in den Stop-Aufruf stecken.

//Effekt:
for (var obj in FindObjects(Find_Distance(100)))
RemoveEffect("Falle", obj);

//Lokale Variabeln:
for (var obj in FindObjects(Find_Distance(100), Find_ID(FALL)))
if (LocalN("Aktiv", obj)) {
CastObjects(FLAM, 5, 40, GetX(obj)-GetX(), GetY(obj)-GetY());
CastParticles("PSpark", 20, 40, GetX(obj)-GetX(), GetY(obj)-GetY(), 60, 60, RGB(255), RGB(255,255));
Explode(30, obj)
}



Effekte mit idCmdTarget

Wir wissen nun schon viel über Effekte. Aber wir haben das Thema ausgelassen, wo Effekte am meisten vorkommen: Zauber!
Grob gibt es zu Zaubern zu sagen: Jeder Zauber hat eine eigene ID und ist ein eigenens Objekt. Darin finden sich alle Funktionen, die der Zauber so benötigt. Wenn ein Zauberer einen Zauber sprechen will, wird dazu das Zauberobjekt an der Position des Zauberers erschaffen, aktiviert und dann entfernt es sich. Kein großes Thema, aber das gilt es zu berücksichtigen - vor allem das Entfernen, sonst hängen bald überall in der Landschaft Zauber herum. Und noch eine Feinheit gibt es: Wenn der Zauber geklappt hat, muss 1 zurückgegeben werden, und wenn er fehlschlägt, 0.

Was wollen wir also tun? Wir nehmen einen Zauber, der vom Grundgerüst recht einfach ist: Wir suchen einen Gegner und belegen ihn mit einem Fluch, eben einem Effekt. Nach einer gewissen Zeit lassen wir den Clonk explodieren. Nicht gerade die feine Art, seinen Gegner zu ärgern, aber durchaus amüsant.

Unseren Zauber nennen wir diesmal ExplosivNSpell. NSpell markiert einen negativen Zauber - und der Gegner wird ihn definitiv als solchen empfinden. Außerdem kann man so einen Gegenzauber schreiben, der alle negativen Effekte (mit NSpell im Namen) entfernt.
Wenn der Zauber ausgeführt wird, wird im Zauber Activate() aufgerufen - dieselbe Funktion, wie wenn man Doppelgraben drückt, wenn sich das Objekt im Inventar befindet.
Zu guter Letzt muss unser Zauber noch die Kategorie C4D_Magic haben, damit es im Zaubermenü auftaucht. Außerdem muss er noch in den Szenarioeigenschaften aktiviert werden (Eigenschaften -> Ausrüstung -> Erweitert -> Zauber).

Dann mal an den Script:

/*-- Explosiv-Fluch --*/

#strict 2

protected func Activate(object pClonk) {
//Gegner suchen. OCF_CrewMember findet alle lebenden Clonks jedes Typs (Clonk, Ritter, Assassine, usw)
//Find_Hostile(iPlr) sucht nur verfeindete Spieler.
var pGegner = FindObject2(Find_OCF(OCF_CrewMember), Find_Distance(200), Find_Hostile(GetOwner(pClonk)));
//Keiner in der Nähe?
if (!pGegner) {
Sound("Error");
RemoveObject();
return;
}
//Gefunden! Gleich mal Sound abspielen und Effekt anhängen
Sound("Magic*");
AddEffect("ExplosivNSpell", pGegner, 1, 1, this, 0, pClonk);
RemoveObject();
return 1;
}

//Der Effekt beginnt. "var1" wurde hier "pCaster" genannt und ist der ausführende Clonk
protected func FxExplosivNSpellStart(object pTarget, int iEffectNumber, int iTemp, pCaster) {
if (iTemp) return;
//Partikel-Linie vom Caster zum Opfer, mit Farbübergang von Caster-Farbe zu Opfer-Fabe
DrawParticleLine("PSpark", GetX(pCaster) - GetX(), GetY(pCaster) - GetY(), GetX(pTarget) - GetX(), GetY(pTarget) - GetY(), 5, 50, GetColorDw(pCaster), GetColorDw(pTarget));
//Und noch ein paar Partikel am Opfer
CastParticles("PSpark", 10, 40, GetX(pTarget) - GetX(), GetY(pTarget) - GetY(), 40, 40, GetColorDw(pCaster), GetColorDw(pCaster));
//Den Besitzer speichern wir noch zur Killverfolgung
EffectVar(0, pTarget, iEffectNumber) = GetOwner(pCaster);
}

//Tolle Partikeleffekte für den Clonk
protected func FxExplosivNSpellTimer(object pTarget, int iEffectNumber, int iTime) {
var r = Random(360);
CreateParticle("PSpark", GetX(pTarget) + Sin(r, 40) - GetX(), GetY(pTarget) - Cos(r, 40) - GetY(), -Sin(r, 10), Cos(r, 10), 30, GetColorDw(pTarget));
//Und nach 350 Frames... Kabumm!
if (iTime > 350)
return -1;
}

//Effekt ist weg! Hochjagen:
protected func FxExplosivNSpellStop(object pTarget, int iEffectNumber, bool fTemp) {
if (fTemp) return;
//Nicht der Clonk explodiert, sondern ein Stein an seiner Stelle.
//Besitzer ist der Besitzer des Zauberers, den haben wir ja gespeichert.
Explode(40, CreateObject(ROCK, GetX(pTarget) - GetX(), GetY(pTarget) - GetY(), EffectVar(0, pTarget, iEffectNumber)));
}


Das Grundgerüst wurde noch mit einigen zusätzlichen Partikeleffekten ausgestattet, schließlich soll unser Zauber auch nett aussehen. Nur nicht davon verwirren lassen.
Wir testen ihn aus. Spiel starten, Gegner hinzufügen, verfeinden und Zauber sprechen! Eine Partikelline bahnt sich von unserem zum gegnerischen Clonk, der Zauber funktioniert.
Oder doch nicht? Alle weiteren Partikel-Effekte bleiben aus, obwohl FxExplosivNSpellTimer regelmäßig welche erzeugt. Und nach 350 Frames steht der Gegner immer noch quicklebendig da, obwohl er von einer Explosion hinfortgeschleudert werden müsste.

Irgendetwas funktioniert nicht. Nun gut, schauen wir uns den Script an:
Wir haben den Effekt hinzugefügt wie immer, mit der Ausnahme, dass der Effekt diesmal als 2. Parameter (das Zielobjekt) nicht this, sondern pGegner erhalten hat. Klar, schließlich soll nicht das Zauberobjekt verflucht werden, sondern der gegnerische Clonk.
Aber da ist noch ein this - als 5. Parameter. Das war der, der bestimmt, wo die Engine die Effekt-Funktionen findet. Also sucht die Engine im Zauber nach den Effektfunktionen. Dort stehen sie auch - die Engine findet sie aber trotzdem nicht, schließlich hat sich das Zauberobjekt direkt danach entfernt!
Doch es gibt Hoffnung: Man könnte nun alle Effekte als global deklarieren, aber es geht auch anders. Bisher haben wir den 6. Parameter elegant umschifft: idCmdTarget. Hier kann die Objektdefinition angegeben werden, wo die Engine die Funktionen findet. Sie sucht dann nicht nach dem Objekt, sondern nach der Definition, und die findet sie natürlich auch. Auch wenn das Objekt sich löscht.

Hier genügt es, einfach GetID() anzugeben. Damit bekommen wir unweigerlich die Zauberobjekt-ID, und die benötigen wir ja. Da nun der Effekt abläuft, als wäre er global, müssen wir aber die Partikeleffekte umändern: Die ganzen GetX()- und GetY()-Abzüge zur korrekten Darstellung sind nicht mehr nötig und fliegen raus.

/*-- Explosiv-Fluch --*/

#strict 2

protected func Activate(object pClonk) {
//Gegner suchen. OCF_CrewMember findet alle lebenden Clonks jedes Typs (Clonk, Ritter, Assassine, usw)
//Find_Hostile(iPlr) sucht nur verfeindete Spieler.
var pGegner = FindObject2(Find_OCF(OCF_CrewMember), Find_Distance(200), Find_Hostile(GetOwner(pClonk)));
//Keiner in der Nähe?
if (!pGegner) {
Sound("Error");
RemoveObject();
return;
}
//Gefunden! Gleich mal Sound abspielen und Effekt anhängen
Sound("Magic*");
AddEffect("ExplosivNSpell", pGegner, 1, 1, 0, GetID(), pClonk);
RemoveObject();
return 1;
}

//Der Effekt beginnt. "var1" wurde hier "pCaster" genannt und ist der ausführende Clonk
protected func FxExplosivNSpellStart(object pTarget, int iEffectNumber, int iTemp, pCaster) {
if (iTemp) return;
//Partikel-Linie vom Caster zum Opfer, mit Farbübergang von Caster-Farbe zu Opfer-Fabe
DrawParticleLine("PSpark", GetX(pCaster), GetY(pCaster), GetX(pTarget), GetY(pTarget), 5, 50, GetColorDw(pCaster), GetColorDw(pTarget));
//Und noch ein paar Partikel am Opfer
CastParticles("PSpark", 10, 40, GetX(pTarget), GetY(pTarget), 40, 40, GetColorDw(pCaster), GetColorDw(pCaster));
//Den Besitzer speichern wir noch zur Killverfolgung
EffectVar(0, pTarget, iEffectNumber) = GetOwner(pCaster);
}

//Tolle Partikeleffekte für den Clonk
protected func FxExplosivNSpellTimer(object pTarget, int iEffectNumber, int iTime) {
var r = Random(360);
CreateParticle("PSpark", GetX(pTarget) + Sin(r, 40), GetY(pTarget) - Cos(r, 40), -Sin(r, 10), Cos(r, 10), 30, GetColorDw(pTarget));
//Und nach 350 Frames... Kabumm!
if (iTime > 350)
return -1;
}

//Effekt ist weg! Hochjagen:
protected func FxExplosivNSpellStop(object pTarget, int iEffectNumber, bool fTemp) {
if (fTemp) return;
//Nicht der Clonk explodiert, sondern ein Stein an seiner Stelle.
//Besitzer ist der Besitzer des Zauberers, den haben wir ja gespeichert.
Explode(40, CreateObject(ROCK, GetX(pTarget), GetY(pTarget), EffectVar(0, pTarget, iEffectNumber)));
}




Dieses ausführliche Tutorial wurde von Limeox verfasst und kommt ursprünglich vom CNDG


4 Kommentare


03.04.2014 10:48 von Gamer:
Naja, das CNDG ist ja immernoch readable. Achja, all die Tutorials die ich damals geschrieben habe.. war 2. aktivster User, 392 Topics, 1492 Posts! Ich finde, im Original ist die Formatierung schöner, warum also nicht gleich die Tutorials auf http://cndg.forumieren.de/f3-tutorials anschauen? Syntaxhighlighting ist anders als im c4scripter von den Farben her, aber immernoch schön. Das allgegenwärtige Blau gefällt mir hier nicht, da find ich klassisches schwarz auf weiß schöner. Die OpenClonk Wiki wäre besser dafür geeignet, wenn die ne Tutorials Section aufmachen würde.  

27.03.2014 20:08 von Tyron:
Brandneues Feature, gerade erst 6 Stunden alt!
Zuletzt geändert: 27.03.2014 20:09

27.03.2014 18:12 von Pitri:
Ah, vom c4script-Tag wusste ich noch garnicht. Ist der neu oder einfach nur nicht in der Anleitung vermerkt?

27.03.2014 14:55 von Tyron:
Sehr nettes tutorial! Hab mir die Freiheit genommen, [c4script] und richtige Einrückung im Code einzubauen.
Zuletzt geändert: 27.03.2014 15:05