Ich werde in dem Tut das Wort Script bewusst nehmen, auch wenn ich weiß das es prinzipiell eine Struktur/Klasse ist, aber das Wort Script hat sich nun einmal durchgesetzt, weswegen es nur verwirrend wäre alles wieder anders zu benennen.
Auch werde ich es vermeiden Fachbegriffe zu verwenden und alles etwas einsteigerfreundlicher zu beschreiben, wer also hier ein Vortrag für angehende Informatiker erwartet sollte bitte den Thread wieder zu machen.
Und los gehts:
Eigentlich ist so ein Script bauen recht einfach, hier mal ne kleine "Anleitung" wie man sowas anfangen kann...
Als erstes brauchen wir ein Grundgerüst für den Boss.
Das sieht meist so aus:
Code:
#include "ScriptPCH.h" class boss_meiner : public CreatureScript { public: boss_meiner() : CreatureScript("boss_meiner") { } CreatureAI* GetAI(Creature* pCreature) const { return new boss_meinerAI (pCreature); } struct boss_meinerAI : public ScriptedAI { boss_meinerAI (Creature *c) : ScriptedAI(c) { } void Reset() { } void EnterCombat(Unit* /*who*/) { } void JustDied(Unit* /*killer*/) { } void KilledUnit(Unit *) { } void UpdateAI(const uint32 diff) { if (!UpdateVictim()) return; } }; }; void AddSC_boss_meiner() { new boss_meiner(); }
- boss_meiner() : CreatureScript("boss_meiner") { }
Hier definiert man den ScriptNamen, in dem Fall heißt er in der ScriptName für die DB "boss_meiner".
- boss_meinerAI (Creature *c) : ScriptedAI(c)
Das ist die "Initialroutine", sie wird am Anfang angefahren und kann verwendet werden um feste Variablen zu setzen (zum Beispiel um den Zugriff auf unser InstanzScript zu bekommen). Auch kann man hier sagen was für eine Art Script es ist.
In dem Fall ist es ne Scripted-AI, wenn der Boss in einer Instanz stehen soll und nicht irgendwo anders in der Welt verwendet wird, können wir auch eine BossAI nehmen, dazu aber vllt. später mehr.
- void Reset()
Diese Routine wird aufgerufen, wenn der Boss resettet wird, also Quasi nachdem ein Wipe war, oder nachdem der Init für das Script kam.
- void EnterCombat(Unit* /*who*/)
Das ist die Routine die aufgerufen wird, wenn er in den Kampf eintritt, dort kann man Texte, Effekte oder was auch immer beim Kampfeintritt gewünscht ist einbauen
- void JustDied(Unit* /*killer*/)
Hat nix mit DiätCola oder so zu tun, sondern ist die Routine, die gefahren wird, wenn der Boss tot ist.
- void UpdateAI(const uint32 diff)
Sehr wichtiges Ding, die Routine wird alle 50ms aufgerufen, dort legt man alle Timerabfragen für Spells, Damage, was noch so passiert rein.
- void KilledUnit(Unit *)
Eine lustige Routine, hier kann man bestimmen was passieren soll, wenn ein Spieler tot ist.
Nun ist dieses Grundgerüst sehr langweilig, weil nix drinnen steht was passieren könnte...
Also füllen wir den Boss mit Leben.
Als erstes würde ich da mal ein paar Texte einfügen.
Einen Text um die Player zu warnen das der Boss nun infight geht, einen um Player verhöhnen, falls einer von ihnen stirbt und einen falls der Boss stirbt.
Dann mal los.
Um einen Boss etwas sagen lassen zu können haben wir zwei Varianten zur Auswahl:
1. Über die Datenbank
2. Über das Script
Natürlich können wir hier auch differenzieren, ob er schreien, sagen oder emotes machen soll (das sind so die wichtigsten, mehr kann man aus der Object.cpp ersehen).
Zum Yell (schreien) nehmen wir me->MonsterYell("TEXT",0,0);
Zum Say (sagen) nehmen wir me->MonsterSay("TEXT",0,0);
Für unseren Boss können wir vorerst ruhig Hardcoding nehmen, wer später lust hat kanns auch in die DB schreiben, der Unterschied ist man muss hier eine ID für seinen Text nutzen, beim Hardcoding passiert das nicht, weil wir den text im Scripts haben.
Okay, ich würde jetzt mal als Eintritt in den Kampf eine einfache Kampfansage bauen.
Das sehe dann so aus:
Code:
void EnterCombat(Unit* /*who*/) { me->MonsterYell("Ihr wagt es mich zu stoeren.", 0, 0); me->MonsterYell("So spuert meinen Zorn", 0, 0); }
(Leider könnt ihr keine Umlaute äöü nutzen, weil sonst einfach kein Text angezeigt wird, das ist aber verschmerzlich.
Das ich den Parameter "who" auskommentiert habe ist ganz einfach zu erklären: Ich benutze ihn in dem Fall nicht und ein guter C++ Compiler sollte bei dem Aufruf der Routine damit dann auch keine Parameter übergeben und uns somit die Routine ein bisschen schneller anfahren, das ist zwar nur marginal, dennoch kann es sich bei recht umfangreichen Routinen ziemlich addieren.)
Nun einen netten Text falls ein Player stirbt.
Code:
void KilledUnit(Unit *) { me->MonsterYell("Das war zu einfach!", 0, 0); }
Dazu benörigen wir einen Random-generator und eine IF oder einen Switch.
Code:
void KilledUnit(Unit *) { switch (urand(1,3)) { case 1: me->MonsterYell("Das war zu einfach!", 0, 0); break; case 2: me->MonsterYell("Ihr seid zu schwach!", 0, 0); break; case 3: me->MonsterYell("Schon muede?", 0, 0); break; } }
Nun weisen wir unseren Boss noch an, das er am Ende auch noch etwas sagt und schon haben wir leben in der Bude.
Code:
void JustDied(Unit* /*killer*/) { me->MonsterSay("Ich habe versagt...",0,0); }
Code:
#include "ScriptPCH.h" class boss_meiner : public CreatureScript { public: boss_meiner() : CreatureScript("boss_meiner") { } CreatureAI* GetAI(Creature* pCreature) const { return new boss_meinerAI (pCreature); } struct boss_meinerAI : public ScriptedAI { boss_meinerAI (Creature *c) : ScriptedAI(c) { } void Reset() { } void EnterCombat(Unit* /*who*/) { me->MonsterYell("Ihr wagt es mich zu stoeren.", 0, 0); me->MonsterYell("So spuert meinen Zorn", 0, 0); } void JustDied(Unit* /*killer*/) { me->MonsterSay("Ich habe versagt...",0,0); } void KilledUnit(Unit *) { switch (urand(1,3)) { case 1: me->MonsterYell("Das war zu einfach!", 0, 0); break; case 2: me->MonsterYell("Ihr seid zu schwach!", 0, 0); break; case 3: me->MonsterYell("Schon muede?", 0, 0); break; } } void UpdateAI(const uint32 diff) { if (!UpdateVictim()) return; DoMeleeAttackIfReady(); //Sorgt dafür das kämpft } }; }; void AddSC_boss_meiner() { new boss_meiner(); }
Für den eintritt in den Kampf nehmen wir mal nen Effekt-Spell, der noch keine Auswirkungen hat, aber schön aussieht.
Ab dann werden wir 2 Spells im Timer casten.
Code:
#include "ScriptPCH.h" enum Spells { SPELL_BALNAZZAR = 17288, SPELL_STOMP = 66330, SPELL_EXPLOSION = 69839 }; class boss_meiner : public CreatureScript { public: boss_meiner() : CreatureScript("boss_meiner") { } CreatureAI* GetAI(Creature* pCreature) const { return new boss_meinerAI (pCreature); } struct boss_meinerAI : public ScriptedAI { boss_meinerAI (Creature *c) : ScriptedAI(c) { } uint32 t_stomp; uint32 t_explosion; void Reset() { t_stomp = 3000; t_explosion = 10000; } void EnterCombat(Unit* /*who*/) { me->MonsterYell("Ihr wagt es mich zu stoeren.", 0, 0); me->MonsterYell("So spuert meinen Zorn", 0, 0); me->CastSpell (me,SPELL_BALNAZZAR,false); } void JustDied(Unit* /*killer*/) { me->MonsterSay("Ich habe versagt...",0,0); } void KilledUnit(Unit *) { switch (urand(1,3)) { case 1: me->MonsterYell("Das war zu einfach!", 0, 0); break; case 2: me->MonsterYell("Ihr seid zu schwach!", 0, 0); break; case 3: me->MonsterYell("Schon muede?", 0, 0); break; } } void UpdateAI(const uint32 diff) { if (!UpdateVictim()) return; if (t_stomp <= diff) { DoCast(me->getVictim(), SPELL_STOMP); t_stomp = urand(3000,6000); } else t_stomp -= diff; if (t_explosion <= diff) { DoCast(me->getVictim(), SPELL_EXPLOSION); t_explosion = 10000; } else t_explosion -= diff; DoMeleeAttackIfReady(); } }; }; void AddSC_boss_meiner() { new boss_meiner(); }
Der Enum ist dazu da um unsere SPELL_FOO eine SpellID zuzuweisen.
Warum man das macht ist schnell geklärt:
Es ist übersichtlicher, als wenn man beim Cast immer die ID angeben muss, ausserdem kann man schnell mal den Spell wechseln.
Nun zur UpdateAI().
Im wesentlichen hab ich unsere zwei Spells mit Timern eingebaut.
Die TimerWerte werden in unserer Reset-Routine eingestellt, im UpdateAI heruntergezählt und anschliessend wieder neu gesetzt.
Setzt man sie nicht neu, macht er den Spell nur einmal.
Code:
if (t_stomp <= diff) { DoCast(me->getVictim(), SPELL_STOMP); t_stomp = urand(3000,6000); } else t_stomp -= diff;
Bei Explosion nehmen wir hier einen festen Wert von immer 10 Sekunden.
Code:
if (t_explosion <= diff) { DoCast(me->getVictim(), SPELL_EXPLOSION); t_explosion = 10000; } else t_explosion -= diff;
if (t_explosion <= diff) //Wenn t_explosion kleiner als diff dann
{
DoCast(me->getVictim(), SPELL_EXPLOSION); //Caste meinen Spell
t_explosion = 10000; //und setze den Timer zurück
}else t_explosion -= diff; //Wenn nicht zähle weiter herunter
10000 bedeutet 10Sekunden.
Tjoa und das wars dann auch schon...
Wer alles bis hier hin verstanden hat sollte schonmal kleine BossScripts schreiben können, wer nicht muss nochmal lesen
Bis hier war alles noch recht einfach gehalten, denn unser Boss machte ja nichts weiter als nur ein paar kleine Spells.
Weil aber einfache Spells auf die Dauer wenig herausforderung bieten können wir ihn ja auch mal ein paar kleine Adds spawnen lassen.
Für den Anfang reichen uns dumme Adds ohne Script.
Für diesen Vorgang nutzen wir den Befehl:
me->SummonCreature();
Leider will dieser X,Y,Z Coordinaten, welche wir uns entweder manuell besorgen (mit GPS) oder einfach von Boss nehmen.
Bei der letzteren Variante spawned der NPC aber an der Position des Bosses.
Was aber hier nicht stören sollte, denn es ist ja nur ein dummes Add.
Code:
enum Spells { SPELL_BALNAZZAR = 17288, SPELL_STOMP = 66330, SPELL_EXPLOSION = 69839 }; enum Adds { ADD_BOESER_WOLF = 12345 //(Die ID 12345 ist fiktiv, also bitte durch eine reelle ersetzen) }; class boss_meiner : public CreatureScript { public: boss_meiner() : CreatureScript("boss_meiner") { } CreatureAI* GetAI(Creature* pCreature) const { return new boss_meinerAI (pCreature); } struct boss_meinerAI : public ScriptedAI { boss_meinerAI (Creature *c) : ScriptedAI(c) { } uint32 t_stomp; uint32 t_explosion; uint32 t_add; void Reset() { t_stomp = 3000; t_explosion = 10000; t_add = 11000; } void EnterCombat(Unit* /*who*/) { me->MonsterYell("Ihr wagt es mich zu stoeren.", 0, 0); me->MonsterYell("So spuert meinen Zorn", 0, 0); me->CastSpell (me,SPELL_BALNAZZAR,false); } void JustDied(Unit* /*killer*/) { me->MonsterSay("Ich habe versagt...",0,0); } void KilledUnit(Unit *) { switch (urand(1,3)) { case 1: me->MonsterYell("Das war zu einfach!", 0, 0); break; case 2: me->MonsterYell("Ihr seid zu schwach!", 0, 0); break; case 3: me->MonsterYell("Schon muede?", 0, 0); break; } } void UpdateAI(const uint32 diff) { if (!UpdateVictim()) return; if (t_stomp <= diff) { DoCast(me->getVictim(), SPELL_STOMP); t_stomp = urand(3000,6000); } else t_stomp -= diff; if (t_explosion <= diff) { DoCast(me->getVictim(), SPELL_EXPLOSION); t_explosion = 10000; } else t_explosion -= diff; if (t_add <= diff) { me->SummonCreature (ADD_BOESER_WOLF, me->GetPositionX(), me->GetPositionY(), me->GetPositionZ()); t_add = 11000; } else t_add -= diff; DoMeleeAttackIfReady(); } }; }; void AddSC_boss_meiner() { new boss_meiner(); }
Für alle anderen, ich habe einfach einen Enum(erator) für unsere Add-Sammlung erstellt (welcher hier noch aus einer NPC ID besteht) und lasse den Boss nun alle 11Sekunden einen Add spawnen.
Der Spawn geschieht hiermit.
me->SummonCreature (ADD_BOESER_WOLF, me->GetPositionX(), me->GetPositionY(), me->GetPositionZ());
Wie man sehen kann stellt uns der Boss seine aktuelle Position zur Verfügung, somit erspart es uns einen Fixpunkt zu bauen.
Klar kann man auch die GPS-Koordinaten nutzen und die Adds fix spawnen lassen, aber man muss bedenken das wenn das Add ausserhalb der Range ist nicht angreift.
me->SummonCreature (ADD_BOESER_WOLF, 1.987f, 0.898, -1.9866f);
Die XYZ-Coordinaten werden alle als Float(point) angegeben, weswegen unsere Zahlen hinten ein f brauchen.
ACHTUNG WISSEN
Wer sich nun fragt was ein Floatpoint ist, dem erkläre ich es schnell:
Ein Float ist eine Fließkommazahl, also eine Zahl mit einem Komma an einer beliebigen Stelle.
Das Gegenstück ist ein Int(integer) oder ein uint.
Ein Integer hat kein Komma und ist immer eine Ganzzahl (eine Zahl ohne Komma).
Der direkte Integer kann Vorzeichen +/- enthalten, ein uint hat kein Vorzeichen und ist immer positiv.
Wichtig währe auch zu wissen das ein uint in verschiedenen Größen existiert, die währen:
uint8. uint16, uint32, uint64, uint128-Bit.
Die Bit-Werte geben die maximale Größe einer Zahl an, der der uint speichert.
8=255
16= 65.535
32= 4.294.967.295
usw.
Wer mehr darüber lesen möchte: Integer (Datentyp)
Warum macht man das mit den 8,16,32,64 Bit?
Auch schnell geklärt: Wenn man weiss das eine Zahl niemals größer als 255 ist es Speicherverschwendung ist sie in einer 16 oder gar 128 Bit großen Variable abzulegen, denn egal wie groß der Wert in der Variable ist, der zugesicherte Speicher ist immer fest (als Vereinfachung 8-Bit=3 Stellen 64-Bit=20 Stellen).
Also platzsparend proggen, sonst liegen bald die wichtigsten Daten in der Swap/Auslagerungsdatei, ab dann gibts Performance einbußen.
Mal schauen vielleicht werde ich das ganze demnächst weiterführen
LG GiR-Blunti
PS:
Ich denke das wenn man sich die anderen Scripte anschaut auch schnell begreift, wie man diese korrekt im ScriptLoader und in CMAKE einbindet, weswegen ich es nicht erwähne
Alles ohne Gewähr und Pistole.