Benutzer-Werkzeuge

Webseiten-Werkzeuge


advancedtutorials:armycontrolling

Steuerung von Armeen

Dieses Tutorial beschäftigt sich mit dem Steuern von Armeen. Zuerst gibt es eine kleine Einführung in die „Low-Level“ Kontrollfunktionen wie etwa Defend(). Später soll versucht werden auf Basis dieser Funktionen eine „High-Level“ Kontrollfunktion für eine Armee zu entwerfen, wie etwa TickOffensiveAiController().

Einführung

Im Tutorial Grundlagen über Computergegner hast du sicherlich schon die Funktion MapEditor_SetupAI() kennengelernt. Mit dieser Funktion wird der KI Gegner Armeen erstellen und je nach Einstellung auch angreifen. Allerdings kann man sich nie sicher sein, wann der KI Gegner angreifen wird und wo er angreifen wird. Möchte man aber Angriffe auf einen Spieler genau steuern können, kommt man um Armeen nicht herum. Wie man diese erstellt und einen kurzen Überblick über die Funktionsweise von Armeen, findet ihr im Tutorial Armeen erstellen. Bevor du weiterliest solltest du Folgendes verstanden haben:

  • Wie man Armeen erstellt und Soldaten hinzufügt
  • Verstanden haben, dass eine Armee nichts tut, wenn man sie nicht kontrolliert

Bevor es richtig los geht, wollen wir noch die genannten Begriffe „Low-Level“ und „High-Level“ klären. Frei übersetzt bedeutet das soviel wie „Primitive“ Funktionen und „Erweiterte“ Funktionen. Primitiv sind (Kontroll-)Funktionen, welche nur eine einzige Aufgabe haben: bewege die Armee dort hin, greife den Feind an usw. Erweiterte Funktionen machen dementsprechend mehr, basieren aber meist auf den primitiven. Zum Beispiel: Wenn ein Feind in der Nähe ist, greife ihn an. Ansonsten Laufe zwischen Punkt A und B hin und her. Beginnen soll das Tutorial mit einer intensiven Begutachtung der Primitive:

Primitive Kontrollfunktionen für Armeen

Die primitiven Kontrollfunktionen bietet das Spiel von sich aus und du hast sie sicher schon mal irgendwo gesehen. Einige von ihnen muss man nur einmal aufrufen, andere müssen in einem SimpleJob laufen. Die Parameter sind in der Referenz erklärt und es wird hier nicht weiter darauf eingegangen. Hier geht es um das was die Funktion tut, nicht wie man sie korrekt aufruft.

Zum Verständnis musst du dir noch etwas klar machen. Eine Armee hat einen sogenannten Anker, das ist eine Position, die den Mittelpunkt des Aktionsradius(rodeLength) der Armee angibt. Dieser Anker wird durch die Primitive oft geändert und verschoben und ist auf keinen Fall mit der im Armee-Table angegebenen Position zu verwechseln. Diese Position dort ist fest gewählt. Truppen, die mit EnlargeArmy() erstellt werden, erscheinen an dieser Position und sie hat außerdem noch weitere Bedeutungen, die im Folgenden klar werden sollten.

Advance

Diese Funktion sucht zunächst die nächstgelegene feindliche Einheit und berechnet den Abstand zwischen dieser und dem Armee Anker. Danach wird der Aktionsradius der Armee auf diese Distanz gesetzt. Was bedeutet das? Ganz einfach: Da sich nun eine feindliche Einheit im Aktionsradius befindet, wird sich die Armee in Richtung der feindlichen Einheit bewegen und diese schließlich angreifen. Das besondere an dieser Funktion ist, dass sich die Armee nicht aufteilt, sondern gemeinsam den Feind angreifen wird.

Die Funktion sollte sinnvollerweise in einem Job verwendet werden, da sonst vermutlich nur die erste gefundene Einheit attackiert wird und die Armee danach nichts mehr tut.

Defend

Hierbei handelt es sich definitionsgemäß schon nicht mehr um eine primitive Funktion, denn sie tut eigentlich zwei Dinge. Befindet sich ein Feind im Aktionsradius, wird der Anker auf die Position dieses Feindes verlegt. Dadurch setzt sich die gesamte Armee in Bewegung und greift diesen Feind an. Dabei wird keine Rücksicht auf zu schnelle oder zu langsame Einheiten genommen. Dies wird sich aber nur bei einem sehr großen Aktionsradius spürbar auswirken. Ist der Feind schließlich besiegt oder geflohen (feindliche Einheiten werden nicht verfolgt), wird der Anker auf die ursprüngliche Position zurückverlegt (das ist die Position aus dem Armee-Table).

Auch Defend() sollte in einem SimpleJob gestartet werden. Bei einmaligem Aufruf würde die Funktion absolut nichts tun.

FrontalAttack

FrontalAttack() funktioniert ähnlich wie Advance(). Allerdings wird hier nicht der Aktionsradius vergrößert, sondern der Anker wird auf die nächstgelegene feindliche Einheit verlagert. Das hat auch zur Folge, dass die Armee sich sofort zu dieser Position bewegt und zwar jeder so schnell wie er kann. Über weite Strecken würden die langsamen Kanonen vermutlich viel zu spät eintreffen. Der Einsatz dieser Funktion lohnt sich also nur über kurze Distanz oder bei monotonen Einheitentypen.

Wieder gilt: Ohne SimpleJob ist die Funktion relativ nutzlos.

Retreat

Sollte der Anker der Armee durch äußere Einflüsse geändert worden sein (z.B. FrontalAttack()), kann er hiermit wieder auf die ursprüngliche Position (aus dem Armee-Table) zurückgesetzt werden. Diese Funktion wird erst bei den High-Level Kontrollfunktionen sehr nützlich.

Der Anker muss nur einmal zurückgesetzt werden, daher muss Retreat() nicht in einem SimpleJob laufen.

Redeploy

Redeploy() kann man mit dem Move() für Entities verglichen werden. Der Anker wird auf die angegebene Position neu gesetzt, woraufhin sich die komplette Armee zu dieser Position begibt. Je nach Einstellung greift sie auf dem Weg dorthin Feinde an oder nicht.

Natürlich muss auch diese Funktion nicht in einem SimpleJob laufen, der Anker muss ja nur einmal neu gesetzt werden.

Synchronize

Synchronize() setzt den Anker der zweiten Armee auf die aktuelle Position (das ist weder die Position des Ankers, noch die Position aus dem Armee Table; es ist die Position, an der sich die Armee aktuell befindet) der ersten Armee. Außerdem wird der Aktionsradius der zweiten Armee auf den der ersten gesetzt.

Man kann hier nicht explizit sagen ob mit SimpleJob oder ohne. In den meisten Fällen wird jedoch ein einmalige Aufruf reichen.

Statusabfragen

Um nun eigene High-Level Kontrollfunktionen schreiben zu können, müsste man noch den aktuellen Status der Armee erfragen können. Auch dafür gibt es einige Helferlein. Alle folgenden Funktionen erwarten nur das Armee-Table als Parameter.

HasFullStrength

Diese Funktion gibt true zurück, wenn die Armee mindestens so viele Hauptmänner besitzt, wie unter strength im Armee-Table angegeben.

IsWeak

Das Gegenteil von HasFullStrength(). Gibt true zurück, wenn weniger Hauptmänner in der Armee sind als unter strength angegeben.

IsVeryWeak

Gibt true zurück, wenn weniger als ein Drittel von strength Hauptmänner in der Armee sind.

IsDead

Gibt nur dann true zurück, wenn keine Hauptmänner mehr am Leben sind.

GetNumberOfLeaders

Um es noch genauer herauszufinden, kann man diese Funktion verwenden. Sie gibt die genaue Anzahl an Hauptmännern in der Armee zurück.

GetClosestEntity

Mit dieser Funktion bekommt man die EntityID des nächstgelegenen Feindes innerhalb des Aktionsradius. Außerdem gibt es einen optionalen zweiten Parameter. Hier kann eine Nummer angegeben werden, die anstelle des Aktionsradius verwendet wird. Somit kann man auch auf größere Entfernung nach Gegnern Ausschau halten.

GetPosition

Hiermit kann man sich die aktuelle Position des Armeeankers zurückgeben lassen.

Erweiterte Kontrollfunktionen (Beispiel)

Achtung: Dieser Teil des Tutorials richtet sich an Mapper, die bereits etwas Erfahrung mit Lua haben.

Mit den bisher genannten Funktionen kann man sich nun individuelle Kontrollfunktionen schreiben, dessen Fantasie eigentlich keine Grenzen gesetzt sind. Eine bekannte Funktion sollte TickOffensiveAiController() sein. Sie wird z.B. auch von den Armeen verwendet, die durch MapEditor_SetupAI() erzeugt werden. Doch hier soll nicht weiter darauf eingegangen werden, denn die Funktion wurde dort ausreichend behandelt. Es soll hier viel mehr ein kleines Beispiel entworfen werden, um die generelle Funktionsweise solcher Funktionen zu erläutern und den Anreiz geben, eigene Ideen umzusetzen.

Aufgabenstellung

Für das Beispiel stellen wir uns folgende Situation vor: Der menschliche Spieler sowie ein KI-Spieler sind untereinander verfeindet. Der KI Gegner verhält sich normalerweise eher passiv und verteidigt sich nur gegen Angriffe vor seinen Stadtmauern. Doch gibt es ganz in der Nähe ein Rohstoffvorkommen, welches der KI Spieler für sich beansprucht. Sollte der menschliche Spieler sich diesen Vorkommen nähern, wird die KI Armee dort angreifen. Sollte die Armee dabei stark geschwächt, zieht sie sich hinter die Stadtmauern zurück und wartet dort, bis sie wieder volle Stärke hat.

Vorüberlegungen

Bevor es an die praktische Umsetzung geht, sollte man sich noch Überlegen wie man aus der Aufgabe möglichst einfach lua-Code erzeugen kann. Dazu schaut man sich zuerst an, wie viele unterschiedliche Aufgaben (Zustände) die Armee hat. In unserem Beispiel wären das:

  • Vor der Stadtmauer verteidigen
  • Rohstoffvorkommen angreifen
  • Hinter den Stadtmauern erholen

Außerdem führen wir hier noch einen weiteren Zustand ein:

  • Rückzug in die Stadt

Als nächstes überlegt man sich, welche Bedingungen vorherrschen müssen, damit die Armee von einem Zustand in einen anderen übergeht:

  • Die Armee verteidigt so lange, bis Feinde in die Nähe der Rohstoffe gelangen. Dann geht sie in den Angriff über.
  • Die Armee greift so lange an, bis alle Feinde besiegt sind. Dann geht sie wieder in den Verteidigungszustand über.
  • Ist die Armee in einem der Zustände „Verteidigung“ oder „Angriff“ und ist sie stark geschwächt, zieht sie sich zurück.
  • Vom Zustand Rückzug geht die Armee in „Erholung“ über, sobald sie hinter den Stadtmauern ist
  • Sobald die Armee wieder volle Stärke hat, geht sie erneut in die Verteidigung

Als letztes legt man einen Startzustand fest, in dem sich die Armee befindet, wenn man sie gerade erstellt. Wir legen als Startzustand „Verteidigung“ fest. Das dies Sinn macht, wird sich später zeigen.

Das folgende Diagramm veranschaulicht noch einmal unsere Überlegungen. Am Anfang ist es sehr hilfreich, wenn man sich so etwas auf Papier zeichnet, damit man nicht den Überblick verliert.


Diese Grafik wurde erstellt mit AutoEdit

Exkurs

Im Beispiel wird verlangt, dass sich die Armee innerhalb der Stadtmauern erholen soll. Dies kann natürlich mit EnlargeArmy() erfolgen. Allerdings wäre es natürlich schöner, wenn die Armee sich ihre Soldaten tatsächlich kaufen würde, wie es bei MapEditor_SetupAI() geschieht. Dafür gibt es leider keine Comfort-Funktion und so müssen wir uns hier ausnahmsweise auf die „unterste“ Lua-Programmierebene begeben und uns eine eigene kleine Comfort-Funktion schreiben, mit der eine Armee Soldaten kaufen kann.

Der Schlüssel zum Erfolg liegt hier im internen Table AI, welches für die Steuerung von Armeen und KI-Spielern zuständig ist. Wir benötigen hier lediglich zwei Funktionen, die kurz erläutert werden sollen.

AI.Army_BuyLeader

Die Funktion AI.Army_BuyLeader() sorgt bei (korrektem) Aufruf dafür, dass ein Hauptmann eines angegebenen Typs ausgebildet werden soll. Damit hier eine spürbare Wirkung erzielt wird, muss natürlich ein Ausbildungsgebäude für diese Einheit existieren und außerdem müssen genug Rohstoffe vorhanden sein. Denke dabei nicht nur an den einzelnen Hauptmann, sondern auch an die Soldaten, die ebenfalls Rohstoffe kosten. Ein KI Spieler ohne Rohstoffe wird keine Truppen produzieren! Platz im Dorfzentrum wird hingegen nicht benötigt; der KI Spieler ignoriert diese Begrenzungen einfach.

Der Typ des Hauptmanns (Bogen, Schwert, Lanze, etc…) wird nicht etwa mit Hilfe des Tables Entities angegeben, sondern über das Table UpgradeCategories. Mögliche Werte sind hierbei:

  • UpgradeCategories.LeaderBow
  • UpgradeCategories.LeaderCavalry
  • UpgradeCategories.LeaderHeavyCavalry
  • UpgradeCategories.LeaderPoleArm
  • UpgradeCategories.LeaderRifle
  • UpgradeCategories.LeaderSword

Eine Ausnahme bilden Kanonen. Hier muss der Entity-Typ angegeben werden. Hier sind möglich:

  • Entities.PV_Cannon1
  • Entities.PV_Cannon2
  • Entities.PV_Cannon3
  • Entities.PV_Cannon4

Wie man diese Funktion richtig verwendet, kommt später noch. Nun soll noch das Geheimnis um die UpgradeCategories gelüftet werden:

Logic.UpgradeSettlerCategory

Du fragst dich sicherlich warum man hier UpgradeCategories verwenden muss und vor allem wie man bestimmen kann, ob nun Bogenschützen oder doch besser Armbrustschützen ausgebildet werden sollen. Die Antwort darauf ist eigentlich ganz einfach und beruht auf dem Prinzip des Spiels. Der menschliche Spieler kann bei Spielstart nur die schwachen Soldaten ausbilden und muss erst Upgrades erforschen, um bessere Soldaten produzieren zu können. Genauso verhält es sich bei den KI Spielern. Zu Anfang würden sie nur schwache Soldaten ausbilden, bis du explizit über das Script neue Upgrades freischaltest. Um einen bestimmten Truppentyp aufzuwerten, muss die Funktion Logic.UpgradeSettlerCategory() aufgerufen werden. Mit jedem Aufruf wird ein bestimmter Truppentyp um eine Stufe aufgewertet.

Kommen wir jetzt zu etwas Beispielcode, der die Parameter und den korrekten Funktionsaufruf verdeutlicht. Die Armee von Spieler 2 soll später mal Langschwertkämpfer (Schwertkampf 2 Upgrades) und Arbalestenschützen (Bogen 3 Upgrades) enthalten.

-- Wir schreiben uns eine kleine Comfort-Funktion, die wir immer wieder verwenden können
function UpgradeSoldiers(_Player, _Category, _Upgrades)
 
    -- Die Funktion muss so oft aufgerufen werden, wie Upgrades ausgeführt werden sollen
    for i = 1, _Upgrades do
        -- Die Funktion erwartet die Kategorie und die SpielerID als Parameter
        Logic.UpgradeSettlerCategory(_Category, _Player)
    end
 
end
 
-- Später kann man diese Funktion dann irgendwo verwenden, z.B. beim Erstellen des Spielers
function CreatePlayerTwo()
 
    -- Beliebiger anderer Code
    -- ...
 
    -- 2 Upgrades auf Schwertkämpfer für Spieler 2
    UpgradeSoldiers(2, UpgradeCategories.LeaderSword, 2)
    -- 3 Upgrades auf Bogenschützen für Spieler 2
    UpgradeSoldiers(2, UpgradeCategories.LeaderBow, 3)
 
end

AI.Entity_ConnectUnemployedLeaderToArmy

Noch eine Funktion? Ja, denn leider ist es mit dem Aufruf von AI.Army_BuyLeader() noch nicht getan. Zwar wird dann schon mal eine Truppe ausgebildet, dass heißt aber noch nicht, dass sich diese Truppe auch der Armee anschließt. Und genau um dieses Problem zu beheben, gibt es die Funktion AI.Entity_ConnectUnemployedLeaderToArmy().

Damit haben wir alle nötigen Grundkenntnisse, um nun unsere Comfort-Funktion für den Truppenkauf zu erstellen.

-- Die Funktion soll das Armee Table und den Truppentyp übergeben bekommen
function BuyTroop(_Army, _Category)
 
    -- Zuerst den Hauptmann ausbilden
    AI.Army_BuyLeader(_Army.player, _Army.id, _Category)
    -- Dann den Hauptmann zu der Armee hinzufügen
    AI.Entity_ConnectUnemployedLeaderToArmy(_Army.player, _Army.id, _Army.strength)
 
end
 
-- Nun kann man diese Funktion später so verwenden:
BuyTroop(ArmyOne, UpgradeCategories.LeaderSword)
BuyTroop(ArmyOne, UpgradeCategories.LeaderBow)
-- Kanonen sind die Ausnahme, da muss der Entity Typ angegeben werden
BuyTroop(ArmyOne, Entities.PV_Cannon3)

Damit ist dieser Exkurs beendet. Wenn ihr nicht alles verstanden habt, macht das nichts. Sofern ihr wisst wie man die zwei neuen Comfort-Funktionen richtig benutzt reicht das aus, um damit zu arbeiten. Ansonsten bleibt euch immer noch EnlargeArmy(), welches in einigen Situationen sogar Vorteile hat, da z.B. keine Rohstoffe benötigt werden.

Umsetzung in Lua

Kommen wir nun von der Theorie zur Praxis!

Die Scriptnamen

Für unsere Beispiel brauchen wir noch einige benannte ScriptEntities, die die Positionen der Verteidigung, des Angriffs und der Erholung angeben.

  • Den Punkt vor der Stadtmauer, an dem die Armee verteidigen soll, geben wir den Namen „Verteidigung“
  • Den Punkt bei den Rohstoffvorkommen nennen wir „Rohstoffe“
  • Die Rückzugsposition hinter der Stadtmauer nennen wir „Erholung“

Die Armee

Wir brauchen natürlich noch eine Armee, die wir so erzeugen:

function CreateArmyOne()
 
    ArmyOne = {}
    ArmyOne.id                = 1
    ArmyOne.player            = 2
    ArmyOne.strength          = 6
    -- Wir verwenden später Defend() im Verteidigungszustand, daher **muss** die Position hier bei Verteidigung liegen
    ArmyOne.position          = GetPosition("Verteidigung")
    ArmyOne.rodeLength        = 3000
    -- Bewegt sich die Armee von A nach B greift sie alles auf dem Weg dorthin an
    ArmyOne.beAgressive       = true
 
    SetupArmy(ArmyOne)
 
    StartSimpleJob("ControlArmyOne")
 
end
 
function ControlArmyOne()
 
    if Counter.Tick2("ControlArmyOne", 5) then
        -- Job beenden, wenn die Burg zerstört wurde
        if IsDead("Player2") then
            return true
        end
 
        -- Diese Funktion schreiben wir gleich
        DefensiveArmyController(ArmyOne)
    end
 
end

ControlArmyOne

Jetzt geht es endlich richtig los und wir beginnen die Kontrollfunktion zu entwerfen. Dazu holen wir noch mal das Zustandsdiagramm von oben zu Hilfe und nummerieren die Zustände im Uhrzeigersinn durch, beginnend mit dem Startzustand „Verteidigung“. Zum leichteren Verständnis erstellen wir dafür auch gleich ein paar Lua-Variablen:

ARMYSTATE_DEFEND   = 1
ARMYSTATE_ATTACK   = 2
ARMYSTATE_FALLBACK = 3
ARMYSTATE_REFRESH  = 4

Damit müssen wir später nicht die Zahlen auswendig lernen sondern können gleich erkennen worum es eigentlich geht.

Außerdem ergänzen wir in der Funktion CreateArmyOne() das Table ArmyOne um folgende Zeilen:

    -- An dieser Position soll angegriffen werden, wenn sich dort Feinde befinden
    ArmyOne.attackPosition    = GetPosition("Rohstoffe")
    -- Hierhin zieht sich die Armee zurück, wenn sie schwach ist und neue Truppen kaufen soll
    -- (Hier wird nur der Scriptname angegeben, weil wir später eine Comfort-Funktion benutzen, die keine Positionsangabe erlaubt)
    ArmyOne.refreshPosition   = "Erholung"

Wir haben jetzt eine eigene Armee-Table Variable definiert, die von den bekannten Funktionen ignoriert wird. Allerdings können wir sie später in der Kontrollfunktion verwenden. Diese Positionen können sich jetzt bei verschiedenen Armeen unterscheiden, ohne dass wir später die Kontrollfunktion abändern müssen. So wird der Code wiederverwertbar.

Was noch fehlt, ist die Definition des Startzustands. Diesen schreiben wir nicht gleich zu Anfang in das Armee Table. Denn dieser muss jedesmal gleich sein, damit die Funktion funktioniert. Daher fragen wir in der Kontrollfunktion als erstes ab, ob schon eine Zustandsvariable existiert. Wenn nicht, erstellen wir sie und setzen sie auf den Startzustand:

function DefensiveArmyController(_Army)
 
    -- Existiert noch kein Zustand?
    if _Army.state == nil then
        -- Dann Startzustand setzen
        _Army.state = ARMYSTATE_DEFEND
    end
 
end

Jetzt hat unsere Armee also einen eindeutigen Zustand und wir können eine große if-elseif-Fallunterscheidung darüber erstellen:

function DefensiveArmyController(_Army)
 
    -- Existiert noch kein Zustand?
    if _Army.state == nil then
        -- Dann Startzustand setzen
        _Army.state = ARMYSTATE_DEFEND
    end
 
    -- Armee verteidigt
    if _Army.state == ARMYSTATE_DEFEND then
 
    -- Armee greift an
    elseif _Army.state == ARMYSTATE_ATTACK then
 
    -- Armee zieht sich zurück
    elseif _Army.state == ARMYSTATE_FALLBACK then
 
    -- Armee erholt sich
    elseif _Army.state == ARMYSTATE_REFRESH then
 
    end
 
end

Genau so kann man das für jede x-beliebige andere Kontrollfunktion machen. Zuerst Zustände definieren und dann eine Fallunterscheidung darüber.

Als nächstes schaut man sich jeden Zustand an und entscheidet, ob ein Zustandswechsel stattfinden soll oder nicht. Zustandswechsel werden im Diagramm mit den Pfeilen dargestellt. Wenn die Bedingung, die an dem Pfeil steht eintrifft, dann gehe in den nächsten Zustand über. Findet kein Wechsel statt, wird das gemacht, was in dem aktuellen Zustand gemacht werden muss. Wie du dir sicher überlegt hast, ist auch das wieder Fallunterscheidung. Folgender Code zeigt die Funktion mit der Fallunterscheidung über die Zustände und auch die Fallunterscheidung über die Zustandsänderungen:

function DefensiveArmyController(_Army)
 
    -- Existiert noch kein Zustand?
    if _Army.state == nil then
        -- Dann Startzustand setzen
        _Army.state = ARMYSTATE_DEFEND
    end
 
    -- Armee verteidigt
    if _Army.state == ARMYSTATE_DEFEND then
        -- Sind Feinde in der Nähe der Rohstoffe?
        if AreEnemiesInArea(_Army.player, _Army.attackPosition, _Army.rodeLength) then
 
 
        -- Ist die Armee sehr schwach oder tot?
        elseif IsVeryWeak(_Army) or IsDead(_Army) then
 
        -- Kein Zustandswechsel, verteidigen
        else
 
        end
 
    -- Armee greift an
    elseif _Army.state == ARMYSTATE_ATTACK then
        -- Sind Feinde in der Nähe der Rohstoffe?
        if not AreEnemiesInArea(_Army.player, _Army.attackPosition, _Army.rodeLength) then
 
        -- Ist die Armee sehr schwach oder tot?
        elseif IsVeryWeak(_Army) or IsDead(_Army) then
 
        else
 
        end
 
    -- Armee zieht sich zurück
    elseif _Army.state == ARMYSTATE_FALLBACK then
        -- Ist die Armee komplett tot oder nah genug am Stadtzentrum?
        if IsDead(_Army) or IsArmyNear(_Army, _Army.refreshPosition) then
 
        else
 
        end
 
    -- Armee erholt sich
    elseif _Army.state == ARMYSTATE_REFRESH then
        -- Hat die Armee wieder volle Stärke?
        if HasFullStrength(_Army) then
 
        else
 
        end
    end
 
end

So sieht jedes Skelett einer High-Level Kontrollfunktion aus. Eine Fallunterscheidung über Zustände und jeweils eine Fallunterscheidung über Zustandsänderungen. Jetzt müssen nur noch die entsprechenden Anweisungen eingefügt werden und man ist fertig. Unser Beispiel könnte z.B. so aussehen:

function DefensiveArmyController(_Army)
 
    -- Existiert noch kein Zustand?
    if _Army.state == nil then
        -- Dann Startzustand setzen
        _Army.state = ARMYSTATE_DEFEND
    end
 
    -- Armee verteidigt
    if _Army.state == ARMYSTATE_DEFEND then
        -- Sind Feinde in der Nähe der Rohstoffe?
        if AreEnemiesInArea(_Army.player, _Army.attackPosition, _Army.rodeLength) then
            -- Ja, Zustandswechsel
            _Army.state = ARMYSTATE_ATTACK
 
            -- Armee-Anker auf das Zielgebiet verlegen
            Redeploy(_Army, _Army.attackPosition)
 
        -- Ist die Armee sehr schwach oder tot?
        elseif IsVeryWeak(_Army) or IsDead(_Army) then
            -- Ja, Zustandswechsel
            _Army.state = ARMYSTATE_FALLBACK
 
            -- Armee-Anker in die Stadt verlegen
            Redeploy(_Army, GetPosition(_Army.refreshPosition))
 
        -- Kein Zustandswechsel, verteidigen
        else
            Defend(_Army)
        end
 
    -- Armee greift an
    elseif _Army.state == ARMYSTATE_ATTACK then
        -- Sind Feinde in der Nähe der Rohstoffe?
        if not AreEnemiesInArea(_Army.player, _Army.attackPosition, _Army.rodeLength) then
 
            -- Ja, Zustandswechsel
            _Army.state = ARMYSTATE_DEFEND
 
            -- Zurück zur Ausgangsposition
            Retreat(_Army)
 
        -- Ist die Armee sehr schwach oder tot?
        elseif IsVeryWeak(_Army)or IsDead(_Army) then
            -- Ja, Zustandswechsel
            _Army.state = ARMYSTATE_FALLBACK
 
            -- Armee-Anker in die Stadt verlegen
            Redeploy(_Army, GetPosition(_Army.refreshPosition))
 
        -- Kein Zustandswechsel, angreifen
        else
            FrontalAttack(_Army)
        end
 
    -- Armee zieht sich zurück
    elseif _Army.state == ARMYSTATE_FALLBACK then
        -- Ist die Armee komplett tot oder nah genug am Stadtzentrum?
        if IsDead(_Army) or IsArmyNear(_Army, _Army.refreshPosition) then
            _Army.state = ARMYSTATE_REFRESH
        end
 
        -- Hier muss nichts mehr getan werden
 
    -- Armee erholt sich
    elseif _Army.state == ARMYSTATE_REFRESH then
        -- Hat die Armee wieder volle Stärke?
        if HasFullStrength(_Army) then
            -- Zurück und verteidigen
            _Army.state = ARMYSTATE_DEFEND
            Retreat(_Army)
 
        else
            -- Soldaten kaufen, hier der Einfachheit halber nur Schwertkämpfer
            BuyTroop(_Army, UpgradeCategories.LeaderSword)
        end
    end
 
end

Anmerkungen

Es werden die Funktionen AreEnemiesInArea und IsArmyNear verwendet.

Die Funktion kann nun für beliebige Armeen eingesetzt werden. Zum Beispiel könnte Spieler 2 noch eine zweite Armee besitzen, die ebenfalls die selbe Position verteidigt, aber eine andere Stelle angreifen würde. Die zweite Armee würde dann genau wie die erste erstellt werden, wobei nur die attackPosition im Armee-Table geändert werden müsste.

Code ohne Kommentare

Hier noch einmal eine Sammlung der Funktionen, die wir in Laufe des Tutorials erstellt haben.

function UpgradeSoldiers(_Player, _Category, _Upgrades)
 
    for i = 1, _Upgrades do
        Logic.UpgradeSettlerCategory(_Category, _Player)
    end
 
end
function BuyTroop(_Army, _Category)
 
    AI.Army_BuyLeader(_Army.player, _Army.id, _Category)
    AI.Entity_ConnectUnemployedLeaderToArmy(_Army.player, _Army.id, _Army.strength)
 
end
ARMYSTATE_DEFEND   = 1
ARMYSTATE_ATTACK   = 2
ARMYSTATE_FALLBACK = 3
ARMYSTATE_REFRESH  = 4
 
function DefensiveArmyController(_Army)
 
    if _Army.state == nil then
        _Army.state = ARMYSTATE_DEFEND
    end
 
    if _Army.state == ARMYSTATE_DEFEND then
        if AreEnemiesInArea(_Army.player, _Army.attackPosition, _Army.rodeLength) then
            _Army.state = ARMYSTATE_ATTACK
 
            Redeploy(_Army, _Army.attackPosition)
 
        elseif IsVeryWeak(_Army) or IsDead(_Army) then
            _Army.state = ARMYSTATE_FALLBACK
 
            Redeploy(_Army, GetPosition(_Army.refreshPosition))
 
        else
            Defend(_Army)
        end
 
    elseif _Army.state == ARMYSTATE_ATTACK then
        if not AreEnemiesInArea(_Army.player, _Army.attackPosition, _Army.rodeLength) then
            _Army.state = ARMYSTATE_DEFEND
 
            Retreat(_Army)
 
        elseif IsVeryWeak(_Army) or IsDead(_Army) then
            _Army.state = ARMYSTATE_FALLBACK
 
            Redeploy(_Army, GetPosition(_Army.refreshPosition))
 
        else
            FrontalAttack(_Army)
        end
 
    elseif _Army.state == ARMYSTATE_FALLBACK then
        if IsDead(_Army) or IsArmyNear(_Army, _Army.refreshPosition) then
            _Army.state = ARMYSTATE_REFRESH
        end
 
    elseif _Army.state == ARMYSTATE_REFRESH then
        if HasFullStrength(_Army) then
            _Army.state = ARMYSTATE_DEFEND
            Retreat(_Army)
 
        else
            BuyTroop(_Army, UpgradeCategories.LeaderSword)
        end
    end
 
end

Externer Link

advancedtutorials/armycontrolling.txt · Zuletzt geändert: 2021/09/18 19:15 (Externe Bearbeitung)