Im Artikel zur 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 ( 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 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. Im 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 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 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.
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.
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 Simple Job, in dem 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.
Voriges Kapitel: Zähler und Zeitlimits
Nächstes Kapitel: Effektive Kommunikation mit dem Spieler
Zurück nach oben