======Banditenlager und Spawner====== Im Artikel zur [[ scripting:tutorials:level1:enemy_ai |Erstellung einfacher Computergegner]] wurde gezeigt, wie man einen einfachen, aber kompetenten Computergegner definieren kann. Das setzte allerdings voraus, dass er eine Stadt besitzt, mit der er seine Truppen ausheben kann. Sobald man plant, kleinere Quests zu implementieren, wird das zu einer Einschränkung. Oft braucht man nur ein kleines Banditenlager, das einen Torschlüssel oder einen Ressourcenschacht bewacht und möchte dafür nicht ganze Landstriche einer feindlichen Siedlung widmen. Die KI-Truppen in solchen Lagern sollen kostenlos gestellt werden und ohne Rekrutierungsgebäude an einem vorgeschriebenen Spawnpunkt erscheinen. Wie man diesen Anwendungsfall umsetzt, wird in diesem Artikel beschrieben. Dazu definieren wird uns zuerst grobe Eigenschaften des Truppenverbands (im Folgenden **Armee** genannt. In Ebene 3 (FIXME link einfügen) tauchen wir noch tiefer in die Armeesteuerung ab. Hier überlassen wir wieder einiges den Comfortfunktionen). \\ Danach definieren wir, in welchem Maße die Armee respawnen soll und unter welchen Bedingungen der Spawn aufhört (beispielsweise durch Zerstörung des Banditenturms). \\ Zum Schluss legen wir fest, wie sich die Armee verhalten soll: In welchem Radius patroulliert sie um den Spawnpunkt herum? Darf sie angreifen oder bleibt sie defensiv? In diesem Artikel werden viele Parameter, notwendig und optional, genannt werden. Um die Auswirkungen dieser Parameter gut einschätzen zu können, bieten sich Experimente an. Die Beispiele auf dieser Seite können dafür eine gute Grundlage bilden. **Achtung**: Die Verwendung von Armeen setzt voraus, dass für den KI-Spieler, dem die Armee gehört, auch eine **[[ scripting:tutorials:level1:enemy_ai#setupplayerai | KI aktiv]]** ist! ''SetupPlayerAi'' ist also zwingend notwendig! Weil das Spiel abstürzt, wenn eine KI aktiviert wird, ohne, dass der KI-Spieler ein Gebäude besitzt, muss für jeden Spieler mit einer Armee auch mindestens 1 Gebäude auf der Map stehen. \\ Gleichzeitig gibt es Konflikte mit der Funktion [[ scripting:tutorials:level1:enemy_ai#mapeditor_setupai |MapEditor_SetupAI]]. Im [[ scripting:tutorials:level2:bandit_camps#die_armee-basisparameter |Abschnitt zu den Armee-Basisparametern]] ist die Angabe einer Armee-Id zwingend erforderlich. ''MapEditor_SetupAI'' belegt einige dieser Ids, abhängig vom angegebenen ''_Strength''-Wert und "bemächtigt" sich aller Armeen, deren Ids ''MapEditor_SetupAI'' für sich reserviert. Dadurch werden sie der Steuerung durch eigene Skripte entzogen. Die Tabelle unten zeigt die Armee-Ids, die durch ''MapEditor_SetupAI'' nicht mehr zur Verfügung stehen. ^ _Strength ^ Reservierte Armee-Ids ^ | 0 | keine | | 1 | 1 bis 2 | | 2 | 1 bis 4 | | 3 | 1 bis 6 | ---- =====Die Armee-Basisparameter===== Die Armee-Basisparameter definieren den groben Rahmen, in dem sich die respawnende Armee bewegen soll. Dazu zählt, welchem Spieler sie gehört und wo und in welchem Radius sie sich bewegt. All diese Informationen werden in ein Table geschrieben und der Funktion ''SetupArmy(_ArmyTable)'' übergeben. Das Table ''_ArmyTable'' muss folgende Keys mit Werten füllen: ^ Key ^ Value-Typ ^ Bedeutung ^ | **player** | Player Id | Spieler-Id des Spielers, dem die Armee gehören soll | | **id** | Ganze Zahl (0 - 9) | Id der Armee. Es darf pro Spieler-Id und Armee-Id maximal **eine** Armee geben. Somit ist die Anzahl der Armeen pro Spieler auf 10 beschränkt | | **position** | Position (Table der Form ''{X = x, Y = y}'') | Defensive Position der Armee | | **rodeLength** | Number | Radius um ''position'', innerhalb dessen sich die Armee bewegt | | **beAgressive** | Boolean | Legt fest, ob die Armee auf dem Weg zu einem Angriffsziel Gegner angreifen soll (für ausschließlich defensive Armee irrelevant). **true** ist hier eigentlich immer sinnvoll | Um ein Beispiel zu geben, platzieren wir ein ''ScriptEntity'' mit dem Skriptnamen ''"ArmyBanditsSpawn"'' auf der Karte. Auf diesem Punkt soll die Armee erscheinen und diesen auch verteidigen. Zusätzlich setzen wir einen Turm ''CB_Bastille1'' mit dem Skriptnamen ''"ArmyBanditsTower"'' und der Spieler-Id 2 in die Nähe des Spawnpunkts. Die Armee wird dann folgendermaßen aufgesetzt: function FirstMapAction() -- Die Armee soll in unserem Beispiel direkt zu Spielstart erscheinen -- Natürlich kann sie auch erst zu einem späteren Zeitpunkt erschaffen werden -- Wichtig ist nur, dass vor der Armeeerstellung der zugehörige KI-Spieler aktiviert wird! -- Das Banditenlager hat keine Leibeigenen zur Verfügung, muss also nichts Besonderes können -- Die KI muss nur aktiviert sein SetupPlayerAi(2, {}) CreateArmyBandits() end function CreateArmyBandits() -- Wir definieren das Armee-Table -- Um die Armee später steuern zu können, muss dieses Table global sein! ArmyBandits = { player = 2, -- wir wählen die Id 0 -- bei mehreren Armeen für Spieler 2 müssen alle unterschiedliche Ids haben id = 0, position = GetPosition("ArmyBanditsSpawn"), rodeLength = 4000, beAgressive = true } SetupArmy(ArmyBandits) end **Tipp**: Du kannst deinen gewünschten Radius durch [[ scripting:tutorials:level1:place_entities#ambient_sounds |Platzieren eines XS_Ambient]] auf die Position ''position'' ermitteln. Das 100-fache der angegebenen Größe der Ambient-Entity entspricht genau der anzugebenden ''rodeLength'' in ''SetupArmy''. An dieser Stelle weiß das Spiel nur, dass eine Armee existieren soll. Sie enthält aber noch keine Truppen. ---- =====Das Respawnverhalten===== Um die Armee selbstständig respawnen zu lassen, erweitern wir das Armee-Table um einige Parameter, um das Respawn-Verhalten zu definieren. Das erweiterte Armee-Table geben wir dann in die Funktion ''SetupAITroopSpawnGenerator(_SpawnGeneratorName, _ArmyTable)'', wobei der String ''_SpawnGeneratorName'' frei wählbar ist und sich nur nicht doppeln sollte. Das Armee-Table muss um folgende Einträge erweitert werden: ^ Key ^ Value-Typ ^ Bedeutung ^ | **strength** | Integer ≤ 8 | Maximale Anzahl der Hauptmänner, die gleichzeitig in der Armee sein können. Ist nach oben auf 8 begrenzt. Wenn mehr als 8 Hauptmänner in der Armee sind, werden alle überzähligen Truppen durch die KI nicht gesteuert | | **spawnTypes** | Table (Liste) | Liste von Tables, die jeweils den Leader-Typ und die Anzahl der Soldaten für diesen Leader festlegen. Die ''spawnTypes'' werden beim Respawn nacheinander durchlaufen, sodass auch Dopplungen möglich sind, um das Verhältnis verschiedener Truppentypen festzulegen (siehe Beispiel). **Wichtig**: Die Angegebenen Entity-Typen müssen Hauptmänner oder Kanonen sein, da sonst das Spiel abstürzt! | | **endless** | Boolean | Die ''spawnTypes'' werden sequentiell durchlaufen. Ist ''endless'' **false**, endet der Respawn mit dem letzten Eintrag in der ''spawnTypes''-Liste. Ist ''endless'' **true** und das Ende der ''spawnTypes''-Liste erreicht, beginnt der Respawn wieder am Anfang der Liste | | **spawnPos** | Position (Table der Form ''{X = x, Y = y}'') | Position, an der die Truppen erscheinen sollen. In der Regel ist die gleiche Position wie ''position'' die beste Wahl | | **spawnGenerator** | String oder Number | Skriptname oder Entity-Id der Entity, die den Respawn erlaubt. Ist die angegebene Entity zerstört, bricht der Respawn ab | | **maxSpawnAmount** | Integer | Pro Spawn werden maximal ''maxSpawnAmount'' neue Truppen generiert. Falls ''maxSpawnAmount'' größer ist als die Anzahl an Truppen, die laut ''strength'' noch fehlen, wird nur die Anzahl fehlender Truppen gespawnt | | **respawnTime** | Integer, durch 10 teilbar | Falls die Armee weniger als ''strength'' Truppen enthält, werden nach ''respawnTime'' Sekunden maximal ''maxSpawnAmount'' Truppen gespawnt | | **noEnemy** | Boolean | Wenn ''noEnemy'' **true** ist, findet kein Respawn statt, wenn Gegner in der Nähe sind | | **noEnemyDistance** | Number | Wenn ''noEnemy'' **true** ist, findet kein Respawn statt, wenn Gegner sich der Position ''position'' auf unter ''noEnemyDistance'' Siedler-cm nähern | Um unser Beispiel zu erweitern, wollen wir der Armee alle 90 Sekunden zwei neue Truppen hinzufügen lassen, bis die Maximalstärke 8 erreicht ist. Die Armee soll zur Hälfte aus Axtkämpfern, zu einem Viertel aus Banditbogenschützen und zu einem Viertel aus Langspeerträgern bestehen. Das soll nur geschehen, solange ''"ArmyBanditsTower"'' noch steht. Nähern sich Gegner auf unter 2000 Siedler-cm dem Spawnpunkt, soll der Respawn pausiert werden. function CreateArmyBandits() ArmyBandits = { player = 2, id = 0, position = GetPosition("ArmyBanditsSpawn"), rodeLength = 4000, beAgressive = true, strength = 8, -- Die Armee hat zwar die Maximalstärke 8, wir müssen allerdings nicht jede Truppe einzeln definieren -- Da wir weiter unten "endless" auf true setzen, reicht es, das Verhältnis der unterschiedlichen -- Truppentypen anzugeben -- Die Liste enthält Tables, die wiederum den Typ des Hauptmanns und die Anzahl seiner Soldaten angeben -- Wird "endless" auf false gesetzt, endet der Respawn mit dem Ende der Liste. Die Liste kann beliebig -- lang sein spawnTypes = { {Entities.CU_BanditLeaderSword1, 8}, {Entities.CU_BanditLeaderBow1, 4}, {Entities.CU_BanditLeaderSword1, 8}, {Entities.PU_LeaderPoleArm1, 4} }, endless = true, -- Die Truppen sollen an der Position spawnen, die sie hinterher auch verteidigen spawnPos = GetPosition("ArmyBanditsSpawn"), -- Fällt der Banditenturm, endet der Respawn spawnGenerator = "ArmyBanditsTower", -- Maximal 2 Truppen sollen auf ein mal gespawnt werden... maxSpawnAmount = 2, -- ...und das alle 90 Sekunden... respawnTime = 90, --...und das nur, wenn keine Gegner in der Nähe sind... noEnemy = true, --...die sich dem Spawnpunkt auf unter 2000 Scm nähern noEnemyDistance = 2000 } -- SetupArmy ist weiterhin notwendig SetupArmy(ArmyBandits) SetupAITroopSpawnGenerator("ArmyBanditsSpawnGenerator", ArmyBandits) end Zu Beginn wird die Armee einmal komplett gespawnt, also unabhängig von ''maxSpawnAmount'' auf die volle Stärke gebracht. Allerdings führt sie noch kein Kampfverhalten aus, bleibt also an ihrer Ausgangsposition stehen und nimmt nicht den kompletten Radius ein, den sie verteidigen soll. Falls die Armee angreifen soll ist sie dazu ebenfalls noch nicht imstande. ---- =====Das Kampfverhalten===== Damit die Armee eben dieses Kampfverhalten ausführen kann, erweitern wird erneut das Armee-Table mit weiteren Parametern, die unser gewünschtes Kampfverhalten beschreiben. Nach dem Armee- und Spawner-Setup starten wir einen [[ scripting:tutorials:level1:simple_job|Simple Job]], in dem [[ scripting:tutorials:level2:countdowns|alle 10 Sekunden]] das Armee-Kampfverhalten aktualisiert wird. Das ist deshalb notwendig, weil die Armee regelmäßig auf die Aktionen des Spielers reagieren können muss. Ein einmaliger Aufruf eines Verteidigungs-Befehls beispielsweise würde deshalb nicht ausreichen. Für das Armeeverhalten verwenden wir die Funktion ''TickOffensiveAIController''. Sie steuert die Armee nach folgender Logik: Wenn sie nicht volle Stärke besitzt (also weniger Hauptmänner als in ''strength'' angegeben) verteidigt sie einen kleinen Bereich um die angegebene ''position''. Bei voller Stärke vergrößert sich der Verteidigungsradius. Außerdem kann der Armee erlaubt werden, anzugreifen. Dazu muss mindestens eine Position angegeben werden, die die Armee angreifen kann. Das Armee-Table wird um folgende Key-Value-Paare erweitert: ^Key^Value-Typ^Bedeutung^ |**outerDefenseRange**|Zahl|Der Radius des äußeren Verteidigungsrings. Wenn die Armee maximale Stärke hat, verteidigt sie die angegebene Position ''position'' in diesem Umkreis| |**baseDefenseRange**|Zahl|Der Radius des inneren Verteidigungsrings. Die Armee verteidigt sich in diesem Umkreis an der Position ''position'', wenn sie weniger als ''retreatStrength'' Hauptmänner besitzt| |**retreatStrength**|Integer < ''strength''|Wenn weniger als ''retreatStrength'' Hauptmänner in der Armee sind, zieht sie sich in die ''baseDefenseRange'' zurück| |**AttackPos**|Table|Liste an Postionen, aus denen zufällig eine als Angriffsziel für die Armee gewählt wird| |**AttackAllowed**|Boolean|Gibt an, ob die Armee angreifen darf. Falls **true**, wird sie bei voller Stärke eine der Positionen in ''AttackPos'' zufällig auswählen und angreifen| |**pulse**|Boolean|Wenn **true**, darf die KI kurzzeitig ihren Verteidiungsring verlassen, falls sie volle Stärke hat. Dadurch kann sie schwieriger von außerhalb des Rings beschossen werden| Für unser Beispiel machen wir folgende Angaben (unter der Voraussetzung, dass für Spieler 1 ein Hauptquartier namens ''"Player1"'' sowie ein neutrales Dorfzentrum namens ''"VillageCenter"'' auf der Karte existiert): function CreateArmyBandits() ArmyBandits = { player = 2, id = 0, position = GetPosition("ArmyBanditsSpawn"), rodeLength = 4000, beAgressive = true, strength = 8, spawnTypes = { {Entities.CU_BanditLeaderSword1, 8}, {Entities.CU_BanditLeaderBow1, 4}, {Entities.CU_BanditLeaderSword1, 8}, {Entities.PU_LeaderPoleArm1, 4} }, endless = true, spawnPos = GetPosition("ArmyBanditsSpawn"), spawnGenerator = "ArmyBanditsTower", maxSpawnAmount = 2, respawnTime = 90, noEnemy = true, noEnemyDistance = 2000, -- die baseDefenseRange entspricht der Konsistenz zuliebe genau der rodeLength, -- die wir weiter oben angegeben haben baseDefenseRange = 4000, -- wenn die Armee volle Stärke hat, soll sie einen Umkreis von 6000 scm verteidigen... outerDefenseRange = 6000, -- ...und sich wieder zurückziehen, falls sie nur noch 3 oder weniger Hauptmänner besitzt retreatStrength = 3, -- Angriffe sind erlaubt AttackAllowed = true, -- Die Armee soll entweder das Hauptquartier vom Spieler oder die Position eines (neutralen) -- Dorfzentrums angreifen AttackPos = { GetPosition("Player1"), GetPosition("VillageCenter") }, -- Die Truppen sollen außerdem für kurze Zeit außerhalb ihres Radius kämpfen dürfen pulse = true } SetupArmy(ArmyBandits) SetupAITroopSpawnGenerator("ArmyBanditsSpawnGenerator", ArmyBandits) -- Den Armee-Kontrolljob findest du im Codebeispiel weiter unten StartSimpleJob("ControlArmyBandits") end Der Kontrolljob ''ControlArmyBandits'' soll zwei Dinge erfüllen: Er soll zuerst prüfen, ob die Armee, die er steuert, überhaupt noch existiert. Falls sie das tut, soll das im Armee-Table definierte Kampfverhalten mittels ''TickOffensiveAIController'' ausgeführt werden. Andernfalls soll sich der Job selbst beenden. function ControlArmyBandits() -- Die Kontrollfunktion soll nur alle 10 Sekunden aufgerufen werden -- Dazu verwenden wir den Counter, der im letzten Kapitel vorgestellt wurde -- Es ist zwar möglich, die Kontrollfunktion jede Sekunde aufzurufen - das -- ist allerdings verschwendete Performance und lässt die Armee erratisch wirken, -- da sie u.U. jede Sekunde neue Befehle erhält if Counter.Tick2("ControlArmyBanditsCounter", 10) then -- Die Funktion IsAITroopGeneratorDead prüft, ob sowohl alle Truppen der Armee -- als auch ihr Spawngebäude zerstört wurde if IsAITroopGeneratorDead(ArmyBandits) then -- Falls ja, beende den Kontrolljob return true else -- Andernfalls führe das im Armee-Table definierte Verhalten aus TickOffensiveAIController(ArmyBandits) end end end ---- Die "technischen" Aspekte des Skriptens sind damit für diese Ebene abgeschlossen. Im nächsten Kapitel wollen wir die vielen Möglichkeiten, die man als Skripter zur Kommunikation mit dem Spieler hat, anschauen und einem Zweck zuordnen. [[ scripting:tutorials:level2:countdowns | Voriges Kapitel: Zähler und Zeitlimits ]] \\ [[ scripting:tutorials:level2:communication | Nächstes Kapitel: Effektive Kommunikation mit dem Spieler ]] \\ [[ scripting:tutorials:level2:bandit_camps | Zurück nach oben ]]