Dies ist eine alte Version des Dokuments!
Inhaltsverzeichnis
Armeesteuerung
In vorigen Kapiteln wie im Artikel zur Erstellung einfacher Computergegner oder im Artikel zu Banditenlagern und Spawnern haben wir große Teile der Armeesteuerung an Comfortfunktionen übergeben und lediglich Parameter definiert. In diesem Kapitel werden dagegen elementare Kontrollfunktionen für Armeen vorgestellt, wie sie funktionieren und wo ihre Grenzen sind. Für das Verständnis dieses Artikels ist es von Vorteil, mit Banditenlagern und Spawnern vertraut zu sein und damit bereits experimentiert zu haben.
Armeen sind Truppenverbände, die immer im Gesamten gesteuert werden (es werden also nie die einzelnen Truppen angesprochen). Jede Armee hat eine Position (auch Anker genannt) und einen Radius, in dem sie um den Anker herum operiert. Das Gebiet innerhalb des Radius um den Anker werden wir im Folgenden als Aktionsgebiet bezeichnen. Eine Armee wird ausschließlich über das Verschieben des Aktionsgebietes gesteuert. Im Abschnitt zu den Armeesteuerungsfunktionen findest du einige Grafiken, die das Prinzip verdeutlichen.
Der Artikel ist folgendermaßen strukturiert: Zuerst beschreiben wir, wie Armeen aufgesetzt werden. Anschließend listen wir Funktionen, mit denen sich der Status einer Armee abfragen lässt. Darauf folgen die elementaren Kontrollfunktionen, die für die Steuerung einer Armee zur Verfügung stehen und wie diese einzusetzen sind. Zum Schluss wollen wir all diese Funktionen zu einer komplexeren Armeesteuerung vereinen, wie es zum Beispiel TickOffensiveAIController
auch macht.
Aufsetzen einer Armee
Falls du den Abschitt zu Armee-Basisparametern schon kennst, kannst du diesen Abschnitt überspringen. Die Inhalte sind hier der Vollständigkeit halber enthalten.
Armeen werden mit der Funktion SetupArmy(_ArmyTable)
aufgesetzt. Der Anker wird auf die angegebene Position gelegt und der angegebene Radius grenzt das Aktionsgebiet ein. Der Parameter _ArmyTable
ist ein assoziatives Table mit den folgenden Key-Value-Paaren:
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} ) | Anfangsposition der Armee. Diese Position wird von den Kontrollfunktionen, die weiter unten beschrieben sind, als defensive Position verwendet |
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 |
Für einige der Funktionen, die den Armeestatus abfragen, brauchen außerdem eine Angabe der maximalen Armeestärke im Parameter strength
:
strength | Integer ≤ 8 | Anzahl der Truppen, die eine Armee maximal besitzen können soll. Da eine Armee nur höchstens 8 Truppen steuern kann, sollte die Armeestärke 8 nicht überschreiten |
Eine Armee für Spieler 2 kann also beispielsweise so aufgesetzt werden:
function CreateArmy() -- Wir definieren das Armee-Table -- Um die Armee später steuern zu können, muss dieses Table global sein! Army = { 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("PositionArmy"), rodeLength = 4000, beAgressive = true, strength = 6 } SetupArmy(Army) end
Spieler-Id, Armee-Id, und Aggressivität bleiben für jede Armee fest. Um die Armee zu steuern, werden später nur die Position und die rodeLength
, also der Radius, modifiziert.
Achtung: Die Verwendung von Armeen setzt voraus, dass für den KI-Spieler, dem die Armee gehört, auch eine 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 MapEditor_SetupAI. Die Angabe einer Armee-Id ist 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 |
Wichtig: Mit dem Aufruf von SetupArmy
hat die Armee keine Truppen und wird sie auch nicht selbstständig rekrutieren! Es wird dem Spiel lediglich klargemacht, dass der angegebene Spieler eine Armee mit der designierten Id bekommt und ein leerer Container dafür erstellt. Wie die Armee Truppen erhalten kann (sowohl gespawnt als auch rekrutiert) wird im Abschnitt zu Truppenerstellung beschrieben.
Ein Wort zur automatischen Truppenverstärkung
Unabhängig davon, wie wir weiter unten die Armee befüllen, wird die KI automatisch versuchen, angeschlagene Truppen zu verstärken, indem sie sie zu einer Kaserne (falls vorhanden) zurückzieht und dort neue Soldaten anwirbt. Dieses Verhalten ist nicht abstellbar und kann insbesondere bei rekrutierten Truppen für unerwünschtes Verhalten sorgen (mehr dazu siehe im entsprechenden Abschnitt). Solange die KI einen Trupp zur Verstärkung zurückzieht, ist dieser kein Teil der Armee und kann darum auch nicht mehr über Armeefunktionen gesteuert werden. Erst nachdem der Trupp wieder komplett ist, gibt die KI ihn frei und fügt ihn automatisch zurück zur Armee hinzu.
Funktionen zum Armeestatus
Mit diesen Funktionen kannst du den Status deiner Armee abfragen: Wie viele Truppen sind darin noch enthalten und wo ist der nächste Gegner zum aktuellen Anker? Alle diese Funktionen haben gemeinsam, dass sie als (ersten) Parameter das Armeetable erwarten, das wir oben angelegt haben. Denke wie weiter oben beschrieben daran, dass Truppen, die von der KI gerade verstärkt werden, nicht Teil der Armee sind, bis sie wieder volle Truppstärke haben!
IsDead
Mit IsDead(_ArmyTable)
fragst du ab, ob die angegebene Armee besiegt wurde. Eine Armee gilt als besiegt, wenn sie keine Truppen mehr besitzt. Die Funktion gibt einen Boolean zurück. Die komplette Beschreibung findest du hier.
IsAlive
IsAlive(_ArmyTable)
entspricht genau not IsDead(_ArmyTable)
.
HasFullStrength
HasFullStrength(_ArmyTable)
gibt Auskunft darüber, ob die Armee volle Stärke besitzt, das heißt, ob sie über mindestens so viele Hauptmänner verfügt, wie bei strength
im ArmyTable angegeben ist. Der Rückgabewert ist ein Boolean. Näheres dazu findest du hier.
IsWeak
IsWeak(_ArmyTable)
entspricht genau not HasFullStrength(_ArmyTable)
.
IsVeryWeak
Mit IsVeryWeak(_ArmyTable)
wird ermittelt, ob die Armee weniger als ein Drittel ihrer maximalen Truppenstärke besitzt. Beispielsweise haben wir oben eine strength
von 6
angegeben. IsVeryWeak
wäre in dem Fall true, wenn die Armee weniger als 2 Truppen hat. Zur Funktionsreferenz gehts hier entlang.
GetNumberOfLeaders
Wenn du abseits von IsDead
, HasFullStrength
und IsVeryWeak
genaue Auskunft über die Sträke einer Armee abfragen möchtest, kannst du das mit GetNumberOfLeaders(_ArmyTable)
tun. Die Funktion gibt die genaue Anzahl an Hauptmännern zurück, die sich aktuell in der Armee befinden. Der Artikel in der Funktionsreferenz ist hier.
GetClosestEntity
GetClosestEntity(_ArmyTable, _Radius)
gibt die Entity-Id des nächsten Gegners an, der innerhalb des angegebenen _Radius
der aktuellen _position
am nächsten liegt. Ist kein _Radius
angegeben, wird der aktuelle Aktionsradius/rodeLength verwendet. Die genaue Beschreibung der Funktion findest du hier.
GetPosition
GetPosition(_ArmyTable)
bezogen auf die Armee im _ArmyTable
gibt die aktuelle Position des Ankers der Armee zurück. Beachte, dass eine Armee sich auch auf dem Weg zum Anker befinden kann, wodurch die Position der Truppen nicht mehr der Position des Ankers entspricht. Den Referenzartikel findest du hier.
Elementare Funktionen zur Armeesteuerung
Alle Funktionen, die zum Steuern der Armee verwendet werden, müssen ständig wiederholt aufgerufen werden (üblicherweise alle 5 - 10 Sekunden). Das liegt daran, dass jede dieser Funktionen lediglich das Aktionsgebiet verschiebt (also den Anker und den Radius modifiziert). Anschließend wird geprüft, ob alle Truppen der Armee im Aktionsgebiet sind und ob sich Gegner darin befinden. Würden die Funktionen nicht wiederholt aufgerufen werden, würden die Truppen der Armee nicht zuverlässig im Aktionsgebiet stationiert werden und auch nicht gemeinsam einen eindringenden Gegner angreifen.
In der Regel wird also so oder so ähnlich beim Erstellen einer Armee ein SimpleJob
aufgesetzt, der die Armee steuert:
function CreateExampleArmy() -- hier steht der Code für das ArmyTable -- [ ... ] SetupArmy(ExampleArmy) StartSimpleJob("ControlExampleArmy") end function ControlExampleArmy() -- Die Kontrollbefehle, die weiter unten folgen, sollen nur alle 5 Sekunden aufgerufen werden -- Denke daran, jeder Kontrollfunktion einen eigenen Counternamen zu geben if not Counter.Tick2("ControlExampleArmyCounter", 5) then return end -- Wenn die Armee tot ist (= keine Truppen mehr übrig), wird der Kontrolljob beendet -- Natürlich kann auch eine andere Bedingung den Job abbrechen. Beispielsweise kann -- eine Armee aus einem Gebäude spawnen. Dann ist es viel sinnvoller, den Tod des Gebäudes -- statt den der Armee zur Bedingung für das Ende des Kontrolljobs zu definieren if IsDead(ExampleArmy) then return true end -- hier werden nun die Kontrollfunktionen, die wir weiter unten vorstellen, eingefügt. Diese -- können untereinander kombiniert und beispielsweise nur unter verschiedenen -- Voraussetzungen aufgerufen werden. Ein komplexeres Beispiel findest du im letzten Abschnitt -- dieses Artikels -- [ ... ] end
Redeploy
Redeploy(_ArmyTable, _Position, _Radius)
ist die wichtigste Funktion zur Steuerung von Armeen. Sie verhält sich zu Armeen wie Move bzw. Attack zu Entities (letzteres nur bei aktiviertem beAggressive
).
Konkret wird der Anker der Armee an eine neue Position verschoben und ggf. der Aktionsradius verändert. In einem SimpleJob ausgeführt bewegt sich die Armee dann auf dem schnellsten Weg zum neuen Aktionsgebiet. Wenn beAggressive
aktiv ist, werden Feinde auf dem Weg angegriffen. Das Prinzip ist im Bild unterhalb noch einmal dargestellt und soll insbesondere zum Vergleich mit den anderen Kontrollfunktionen dienen.
Defend
Defend(_ArmyTable)
lässt die Armee eine defensive Haltung auf der im Army-Table angegebenen position
einnehmen. Wenn die Armee eine andere Stelle verteidigen soll, muss also der position
-Eintrag im Table angepasst werden.
Wenn ein Gegner das defensive Aktionsgebiet betritt (also position
mit Radius rodeLength
) wird der Anker der Armee auf die Position dieses Gegners verlegt, sodass er angegriffen wird. Der Anker kann diesen defensiven Bereich nicht verlassen und wird zurück auf die position
gelegt, sobald keine Gegner mehr im Bereich sind. Folglich werden fliehende Gegner auch nicht verfolgt. Im Bild unten ist der Gegner durch ein rotes X dargestellt.
FrontalAttack
FrontalAttack(_ArmyTable)
bewirkt, dass eine Armee den nächstgelegenen Gegner zur eigenen position
findet und den Anker auf dessen Position setzt. Der Aktionsradius wird aus rodeLength
übernommen. Die Armee bewegt sich dann auf dem schnellsten Weg zum neuen Aktionsgebiet. Wenn beAggressive
aktiv ist, werden Feinde auf dem Weg angegriffen.
Bedenke, dass sich die Armee nicht automatisch zurückzieht, wenn kein Gegner mehr zu finden ist.
Advance
Im Gegensatz zum Frontalangriff oben ist Advance(_ArmyTable)
sozusagen der „langsame“ Angriff für eine Armee. Auch hier wird der nächstgelegene Gegner zu einer Armee gesucht. Allerdings wird der Anker nicht verschoben, sondern nur der Aktionsradius so weit vergrößert, dass dieser Gegner gerade noch so darin liegt. Das bewirkt, dass die Armee nicht schnell auf den Gegner zurennt, sondern sich langsam auf ihn zu bewegt.
Retreat
Retreat(_ArmyTable, _Radius)
lässt die Armee sich zurück auf die (defensive) position
ziehen, indem der Anker darauf gesetzt wird. Die Angabe eines _Radius
ist optional – wenn er fehlt, wird rodeLength
verwendet.
Synchronize
Synchronize(_ArmyTable1, _ArmyTable2)
ermittelt die Position der Armee mit _ArmyTable1
und setzt den Anker der zweiten Armee mit _ArmyTable2
auf genau diese Position. Beachte, dass die Position vom Armee 1 nicht notwendigerweise der im Table angegebenen position
entsprechen muss, da sie durch Aufrufe von beispielsweise Redeploy
, Defend
oder FrontalAttack
verändert worden sein könnte.
Der Zweck dieser Funktion ist es, eine Armee einfach einer anderen folgen lassen zu können. Es wäre zum Beispiel denkbar, für Armee 1 einen komplexen Controller zu schreiben, dem Armee 2 dann nur folgen soll, anstatt ihren eigenen zu verwenden.
Truppenerstellung und andere wichtige Armeefunktionen
Bisher haben wir nur behandelt, wie Armeen initialisiert und das Aktionsgebiet verschoben werden kann, um sie zu bewegen. Allerdings müssen Armeen auch Truppen enthalten, um im Spiel etwas bewirken zu können.
Dabei gibt es zwei Möglichkeiten, Truppen für eine Armee zu erschaffen: Spawn und Rekrutierung. Beim Spawn entstehen die Truppen augenblicklich an einer beliebigen Position und ohne Voraussetzungen für Ressourcen oder Technologien. Die Rekrutierung setzt passende Rekrutierungsgebäude und Ressourcen voraus. Der konkrete Truppentyp hängt von der Technologiestufe des KI-Spielers ab. Beide Varianten haben ihre Vor- und Nachteile, wobei das Rekrutieren von Truppen viele handfeste Nachteile hat, die wir später deshalb gesondert beschreiben. Wir beginnen mit dem deutlich einfacher zu handhabenden Spawn.
Truppen spawnen
Um Truppen zu spawnen und einer Armee hinzuzufügen, ist die Funktion EnlargeArmy(_ArmyTable, _TroopDescription)
vorgesehen. Zur Doku-Seite geht es hier entlang. Der Parameter _TroopDescription
ist ein assoziatives Table mit folgenden Key-Value-Paaren:
Key | Value-Typ | Bedeutung |
---|---|---|
leaderType | EntityType | Der Entity-Typ des Hauptmanns, für den ein Trupp erschaffen werden soll. Achtung: Wenn hier kein gültiger Entity-Typ angegeben wird, stürzt die Funktion ohne Fehlermeldung ab! |
position | Position (Table der Form {X = x, Y = y} ) | Die Position, an der der Trupp entstehen soll. Falls die Position blockiert ist (beispielsweise durch ein Gebäude oder andere Truppen), wird die nächste freie Position gewählt, um zu vermeiden, dass der Trupp feststeckt. Wenn keine Position angegeben wird, entsteht der Trupp an der position , die im ArmyTable angegeben ist. |
experiencePoints | Integer | Die Erfahrung, mit der der Hauptmann starten soll. In der Doku ist genau angegeben, welche Optionen man hier hat. Wird nichts angegeben, erhält der Hauptmann keine Erfahrung. |
maxNumberOfSoldiers | Integer | Die Anzahl an Soldaten, mit denen der Hauptmann startet und auf die ggf. wieder verstärkt wird. Wenn nichts angegeben wird, wird automatisch ein voller Trupp erstellt. |
minNumberOfSoldiers | Integer | Die Anzahl an Soldaten, ab der die KI versucht, den Trupp an einer Kaserne zu verstärken. Bei 0 wird der Trupp nie automatisch verstärkt. |
Wichtig: Wenn du eine minNumberOfSoldiers
angibst, wird die KI versuchen, den Trupp zu verstärken, sobald seine Größe unter den angegebenen Minimalwert fällt und ein passendes Rekrutierungsgebäude vorhanden ist. Solange der Trupp verstärkt wird, ist er nicht Teil der Armee, was Auswirkungen auf die Funktionen GetNumberOfLeaders
, IsWeak
, IsVeryWeak
, HasFullStrength
und IsDead
hat!
Wir wollen ein Beispiel geben, in dem zwei verschiedene Truppentypen vor dem passenden Militärgebäude gespawnt werden. Dazu muss jeweils ein XD_ScriptEntity
mit dem Namen „BarracksSpawn“
vor einer Kaserne und eines mit dem Namen „ArcherySpawn“
vor einem Schießplatz existieren (zusätzlich zur „PositionSpawnArmy“
irgendwo in der Nähe). Natürlich muss auch die KI für Spieler 2 aktiviert werden.
function CreateSpawnArmy() SpawnArmy = { 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("PositionSpawnArmy"), rodeLength = 4000, beAgressive = true, strength = 6 } SetupArmy(SpawnArmy) local TroopDescriptionSword = { leaderType = Entities.PU_LeaderSword1, position = GetPosition("BarracksSpawn"), experiencePoints = MEDIUM_EXPERIENCE, maxNumberOfSoldiers = 4, minNumberOfSoldiers = 0 } local TroopDescriptionPoleArm = { leaderType = Entities.PU_LeaderPoleArm2, position = GetPosition("BarracksSpawn"), experiencePoints = LOW_EXPERIENCE, maxNumberOfSoldiers = 4, minNumberOfSoldiers = 0 } local TroopDescriptionBow = { leaderType = Entities.PU_LeaderBow1, position = GetPosition("ArcherySpawn"), experiencePoints = HIGH_EXPERIENCE, maxNumberOfSoldiers = 4, minNumberOfSoldiers = 0 } -- Erstelle jeweils 2 Trupps Schwertkämpfer, Speerträger und Bogenschützen for _ = 1, 2 do EnlargeArmy(SpawnArmy, TroopDescriptionSword) EnlargeArmy(SpawnArmy, TroopDescriptionPoleArm) EnlargeArmy(SpawnArmy, TroopDescriptionBow) end end
Im Beispiel für komplexe Armeesteuerung zeigen wir außerdem beispielhaft, wie ein Respawnverhalten umgesetzt werden kann (ähnlich zum |AITroopSpawnGenerator).
Truppen rekrutieren
Für das Rekrutieren von Truppen gibt es leider keine vorgefertigte Comfortfunktion, weshalb wir hier eine bereitstellen:
function ArmyBuyLeader(_ArmyTable, _UpgradeCategory) -- Check if Army is already at full strength if HasFullStrength(_ArmyTable) then return end local NumberOfLeaders = GetNumberOfLeaders(_ArmyTable) -- Try to add a leader to the army first AI.Entity_ConnectUnemployedLeaderToArmy(_ArmyTable.player, _ArmyTable.id, _ArmyTable.strength) -- Check if a new leader could be connected. If yes, no further action is required if GetNumberOfLeaders(_ArmyTable) > NumberOfLeaders then return end -- If no unemployed leader was added, recruit a new one AI.Army_BuyLeader(_ArmyTable.player, _ArmyTable.id, _UpgradeCategory) AI.Entity_ConnectUnemployedLeaderToArmy(_ArmyTable.player, _ArmyTable.id, _ArmyTable.strength) end
Die _UpgradeCategory
ist dabei nicht der Entity-Typ des gewünschten Hauptmanns, sondern seine „Kategorie“, also zum Beispiel ein Schwertkämpfer:
ArmyBuyLeader(RecruitArmy, UpgradeCategories.LeaderSword)
Das hat den Grund, dass sich die KI an ihre Technologiestufe halten muss. Im Skript kann man nur die Truppenkategorie angeben, die rekrutiert werden soll. Der konkrete Truppentyp muss der KI via Technologien bereitgestellt werden. Beachte außerdem, dass die KI für die Rekrutierung von Truppen ausreichend Rohstoffe, aber keinen Bevölkerungsplatz benötigt. Eine Liste aller UpgradeCategories findest du hier.
Achtung: Falls du mit obiger Funktion Kanonen ausbilden lassen willst, musst du wiederum den Entity-Typen angeben, also Entities.PV_Cannon1
usw.
Auch hierfür wollen wir ein kleines Beispiel schreiben. Die Voraussetzungen für das folgende Skript sind eine Map, die folgendes enthält:
- Mindestens eine Kaserne und ein Schießplatz für Spieler 2
- Ein
XD_ScriptEntity
mit dem Namen„PositionRecruitArmy“
- Ausreichend Taler, Holz und Eisen für Spieler 2
- Eine aktive KI für Spieler 2
function CreateRecruitArmy() RecruitArmy = { 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("PositionRecruitArmy"), rodeLength = 4000, beAgressive = true, strength = 6 } SetupArmy(RecruitArmy) -- Rekrutieren muss etwas zeitverzögert stattfinden, da die Hauptmänner sonst nicht korrekt -- zur Armee hinzugefügt werden. Wir starten darum einen SimpleJob, der nach einigen Sekunden -- Verzögerung die Truppen rekrutieren lässt StartSimpleJob("InitRecruitArmy") end function InitRecruitArmy() if Counter.Tick2("InitRecruitArmy", 5) then -- Erstelle jeweils 3 Trupps Schwertkämpfer und Bogenschützen for _ = 1, 3 do ArmyBuyLeader(RecruitArmy, UpgradeCategories.LeaderSword) ArmyBuyLeader(RecruitArmy, UpgradeCategories.LeaderBow) end -- Sobald das passiert ist, beende den Job return true end end
Probleme beim Rekrutieren
Einer KI ihre Truppen über Rekrutierung zu stellen ist die optisch schönere Lösung, bringt aber einige Probleme mit sich. Diese zu umgehen ist mit den Armee-Funktionen, die das Spiel von sich aus bereitstellt, nicht möglich, sondern müssen mit Armee-Funktionen aus der Community behandelt werden. Diese zu besprechen würde an der Stelle aber den Rahmen des Wikis sprengen. Wir gehen darum auf die wichtigsten Nachteile ein, die die Rekrutierung von Truppen mit der Standardarmee mit sich bringt.
Wenig Kontrolle über die Truppentypen
Wenn du dir den Code für die Funktion ArmyBuyLeader
genau anschaust, wirst du sehen, dass nicht notwendigerweise der rekrutierte Hauptmann der Armee hinzugefügt wird, sondern ggf. irgendeiner, der noch nicht bereits einer Armee zugewiesen wurde.
Das liegt daran, dass die Funktion AI.Army_BuyLeader
den rekrutierten Hauptmann nicht automatisch der Armee hinzufügt und außerdem die Entity-Id des Hauptmanns nicht zurückgibt. Dadurch hat man an der Stelle (fast) keine Handhabe, genau den gewünschten Hauptmann der Armee zuzuweisen. Die Rekrutierung von Kanonen, deren Entity-Id beim Aufruf der Funktion noch lange nicht existiert, erschwert die Sache zusätzlich.
Folglich ist die Rekrutierung von beispielsweise reinen Reiterarmeen nicht zuverlässig möglich, wenn auch andere Armeetypen entstehen sollen.
Wenig Kontrolle über das Verstärkungsverhalten
Wie weiter oben angemerkt, wird die KI versuchen, angeschlagene Truppen zurückzuziehen und an der nächsten Kaserne zu verstärken. Bei Spawntruppen lässt sich der Schwellwert der Truppenstärke, ab der das geschieht, präzise einstellen. Rekrutierte Truppen allerdings werden häufig zurückgezogen, sobald sie auch nur einen ihrer Soldaten verloren haben. Das hat zur Folge, dass die KI-Truppen gerade im Angriff sehr ineffizient kämpfen, weil sie oft gar nicht dazu kommen, einen Gegner zu treffen, sondern sich sehr früh wieder zurückziehen.
Wenig Kontrolle über den Ausbildungsort
Wie bei MapEditor_SetupAI hat man auch bei der händischen Armee-Rekrutierung keine Kontrolle darüber, welche Rekrutierungsgebäude die KI benutzt (letztendlich verwendet MapEditor_SetupAI
ebenfalls AI.Army_BuyLeader
). Außerdem ist man wieder auf genau eine Kaserne, einen Schießplatz usw. eingeschränkt, weil die KI aus jedem weiteren Rekrutierungsgebäude der gleichen Gattung nicht rekrutieren wird.
Beispiel für komplexe Armeesteuerung
Wir wollen die Status-, Steuerungs- und Spawnfunktionen, die oben beschrieben sind, zu einer komplexeren Armeesteuerung zusammenbringen. Die Beispielmap, die den lauffähigen Code enthält, kannst du hier herunterladen. Wir verzichten darauf, den kompletten Quellcode hier im Wiki zu posten.
Um die Armeesteuerung zu demonstrieren, entwerfen wir folgenden Rahmen: Vor den Toren Crawfords haben Banditen ein Lager errichtet. Die Truppen von Crawford sollen sich sammeln und die Banditen vertreiben.
Das Verhalten der Banditen soll dabei ganz einfach gehalten sein: Sie sollen nur verteidigen und in regelmäßigen Abständen aus ihren Türmen respawnen. Nach einiger Zeit stoppt der Respawn, sodass Crawford letztendlich die Oberhand gewinnt.
Das Verhalten der Truppen von Crawford soll komplexer sein:
- Während die Armee nicht die volle Truppenzahl umfasst, soll sie eine defensive Position einnehmen
- Auf der defensiven Position wird die Armee durch den Spawn neuer Truppen verstärkt
- Sobald die Armee ihre volle Stärke besitzt, soll sie zu einer offensiven Position vorrücken
- Auf der offensiven Position läuft ein Counter. Sobald dieser abgelaufen ist, greift die Armee an
- Falls sie so viele Truppen verliert, dass sie als „VeryWeak“ gilt, zieht sie sich auf die defensive Position zurück
- Falls keine Gegner mehr im Angriffsgebiet sind, zieht sie sich auf die offensive Position zurück
Beide Armeen sollen den Respawn einstellen, wenn die Spawngebäude zerstört wurden. Der Ablauf wird in diesem Zustandsdiagramm noch einmal verdeutlicht:
Um die Zustände in Lua umzusetzen, programmieren wir einen Zustandsautomaten. Falls du damit noch nicht vertraut bist, lohnt es sich, den Wikipedia-Artikel durchzulesen.
Folgende Positionen und Gebäude setzen wir auf die Karte:
- Für die Crawford-Armee:
- Eine Kaserne mit dem Namen
„Barracks“
und einen Schießplatz mit dem Namen„Archery“
- Vor der Kaserne und dem Schießplatz jeweils ein
ScriptEntity
mit dem Namen„BarracksSpawn“
bzw.„ArcherySpawn“
- Dem Banditenlager abgewandt ein
ScriptEntity
namens„ArmyCrawfordDefense“
und dem Banditenlager zugewandt„ArmyCrawfordOffense“
- Für das Banditenlager:
- Zwei Banditentürme mit den Namen
„BanditTower1“
und„BanditTower2“
- Ein
ScriptEntity
mit dem Namen„ArmyBanditDefense“
Damit dieser Artikel nicht mit Lua-Code überquillt, verweisen wir auf den Download der Demo-Map , in der der komplett kommentierte Code enthalten ist. Im Spiel wird der Zustandswechsel der Crawford-Armee außerdem gesondert angezeigt.
Wichtig sind insbesondere folgende Aspekte:
Definition der Zustände
Wir nummerieren die Zustände einer Armee durch, sodass sie im Skript besser lesbar sind und beim Programmieren Fehler vermieden werden. Dafür definieren wir ein globales Table, das aussagekräftige Namen verwendet:
ArmyStates = { Defense = 1, Offense = 2, Attack = 3 }
In der Kontrollfunktion für die Crawford-Armee prüfen wir dann immer zuerst, in welchem Zustand sich die Armee befindet, fragen dann ab, ob ein Zustandsübergang möglich ist und führen anschließend die dem Zustand zugehörige Aktion aus.
Zusätzliche Parameter im Army-Table
Für beide Armeen brauchen wir zusätzliche Parameter, die über die Basisparameter hinaus gehen. Dazu zählen beispielsweise Parameter, die das Spawnverhalten der Armeen steuern:
spawnDelay = 3, spawnAmount = 2, spawnCounter = 0, spawnCycles = 12, spawnLifelines = {"BanditTower1", "BanditTower2"}
Diese Parameter müssen wir selbst in den jeweiligen eigenen Kontrollfunktionen verwenden - sie haben keinen Einfluss auf die vom Spiel bereitgestellten Funktionen.
Nur eingeschränkt zum Copy-Paste geeignet
Die Kontrollfunktionen, an denen beispielhaft Armeeverhalten gezeigt wird, soll möglichst viele der im Artikel vorgestellten Funktionen demonstrieren und anschaulich machen. Dadurch ist aber nicht gewährleistet, dass sie für deine Map genau passend ist. Wichtig ist, dass du die Funktionen verstehst!
Beispielmap von Play4Fun
Play4Fun hat ebenfalls eine Karte erstellt, die komplexe Armeesteuerung demonstriert. Wenn du dir Armeeverhalten anschauen möchtest, das über den Rahmen dieses Wikis hinaus geht, findest du hier den Downloadlink:
https://www.siedler-maps.de/maps/map-1706.htm
Im nächsten Kapitel wird beschrieben, wie du die KI einzelne Gebäude aufbauen lassen kannst.