Inhaltsverzeichnis

Banditenlager und Spawner

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 (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 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

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.


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 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:

KeyValue-TypBedeutung
outerDefenseRangeZahlDer Radius des äußeren Verteidigungsrings. Wenn die Armee maximale Stärke hat, verteidigt sie die angegebene Position position in diesem Umkreis
baseDefenseRangeZahlDer Radius des inneren Verteidigungsrings. Die Armee verteidigt sich in diesem Umkreis an der Position position, wenn sie weniger als retreatStrength Hauptmänner besitzt
retreatStrengthInteger < strengthWenn weniger als retreatStrength Hauptmänner in der Armee sind, zieht sie sich in die baseDefenseRange zurück
AttackPosTableListe an Postionen, aus denen zufällig eine als Angriffsziel für die Armee gewählt wird
AttackAllowedBooleanGibt 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
pulseBooleanWenn 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