Erstellen Sie eine Hockeyspiel-KI mit Lenkverhalten: Angriff
() translation by (you can also view the original English article)
In diesem Tutorial codieren wir weiterhin künstliche Intelligenz für ein Hockeyspiel unter Verwendung von Steuerungsverhalten und endlichen Zustandsautomaten. In diesem Teil der Serie lernen Sie die KI kennen, die von Spieleinheiten benötigt wird, um einen Angriff zu koordinieren, bei dem es darum geht, den Puck abzufangen und zum Ziel des Gegners zu tragen.
Ein paar Worte zum Angriff
Die Koordination und Durchführung eines Angriffs in einem kooperativen Sportspiel ist eine sehr komplexe Aufgabe. In der realen Welt treffen Menschen, wenn sie ein Hockeyspiel spielen, mehrere Entscheidungen, die auf vielen Variablen beruhen.
Diese Entscheidungen beinhalten Berechnungen und das Verständnis dessen, was vor sich geht. Ein Mensch kann anhand der Aktionen eines anderen Gegners erkennen, warum sich ein Gegner bewegt, zum Beispiel "er bewegt sich, um in einer besseren strategischen Position zu sein". Es ist nicht trivial, dieses Verständnis auf einen Computer zu übertragen.
Wenn wir also versuchen, die KI so zu codieren, dass sie allen menschlichen Nuancen und Wahrnehmungen folgt, wird das Ergebnis ein riesiger und beängstigender Haufen Code sein. Darüber hinaus ist das Ergebnis möglicherweise nicht präzise oder leicht zu ändern.
Aus diesem Grund versucht unsere Angriffs-KI, das Ergebnis einer Gruppe von Menschen zu imitieren, nicht die menschliche Wahrnehmung selbst. Dieser Ansatz führt zu Annäherungen, aber der Code wird leichter zu verstehen und zu optimieren sein. Das Ergebnis ist gut genug für mehrere Anwendungsfälle.
Den Angriff mit Staaten organisieren
Wir werden den Angriffsprozess in kleinere Teile zerlegen, wobei jeder eine ganz bestimmte Aktion ausführt. Diese Teile sind die Zustände eines stapelbasierten endlichen Automaten. Wie zuvor erläutert, erzeugt jeder Zustand eine Lenkkraft, die den Athleten dazu bringt, sich entsprechend zu verhalten.
Die Orchestrierung dieser Zustände und die Bedingungen für den Wechsel zwischen ihnen werden den Angriff definieren. Das folgende Bild zeigt das komplette im Prozess verwendete FSM:



Wie das Bild zeigt, basieren die Bedingungen für den Wechsel zwischen den Zuständen ausschließlich auf der Entfernung und dem Besitz des Pucks. Zum Beispiel hat das team has the puck
oder der puck is too far away
.
Der Angriffsprozess besteht aus vier Zuständen: idle
, attack
, stealPuck
und pursuePuck
. Der idle
zustand wurde bereits im vorherigen Tutorial implementiert und ist der Ausgangspunkt des Prozesses. Von dort aus wechselt ein Athlet zum attack
, wenn das Team den Puck hat, zum stealPuck
, wenn das gegnerische Team den Puck hat, oder zum pursuePuck
, wenn der Puck keinen Besitzer hat und nahe genug ist, um gesammelt zu werden.
Der attack
-Stand repräsentiert eine offensive Bewegung. In diesem Zustand versucht der Athlet, der den Puck trägt (sogenannter leader
), das Ziel des Gegners zu erreichen. Teamkollegen werden mitziehen und versuchen, die Aktion zu unterstützen.
Der stealPuck
-Zustand repräsentiert etwas zwischen einer defensiven und einer offensiven Bewegung. In diesem Zustand konzentriert sich ein Athlet darauf, den Gegner mit dem Puck zu verfolgen. Ziel ist es, den Puck zurückzuerobern, damit das Team wieder angreifen kann.
Schließlich hat der pursuePuck
-Zustand nichts mit Angriff oder Verteidigung zu tun; es wird die Athleten nur führen, wenn der Puck keinen Besitzer hat. In diesem Zustand versucht ein Athlet, den Puck, der sich frei auf dem Spielfeld bewegt, zu bekommen (zum Beispiel nachdem er von einem Schläger getroffen wurde).
Aktualisieren des Ruhezustands
Der zuvor implementierte idle
zustand hatte keine Übergänge. Da dieser Zustand der Ausgangspunkt für die gesamte KI ist, aktualisieren wir ihn und machen ihn in die Lage, in andere Zustände zu wechseln.
Der idle
zustand hat drei Übergänge:



Wenn das Team des Athleten den Puck hat, sollte der idle
aus dem Gehirn geknallt und der attack
sollte vorangetrieben werden. Wenn das gegnerische Team den Puck hat, sollte idle
durch stealPuck
ersetzt werden. Der verbleibende Übergang findet statt, wenn niemand den Puck besitzt und er sich in der Nähe des Athleten befindet; In diesem Fall sollte pursuePuck
in das Gehirn geschoben werden.
Die aktualisierte Version von idle
ist wie folgt (alle anderen Zustände werden später implementiert):
1 |
class Athlete { |
2 |
// (...)
|
3 |
private function idle() :void { |
4 |
var aPuck :Puck = getPuck(); |
5 |
|
6 |
stopAndlookAt(aPuck); |
7 |
|
8 |
// This is a hack to help test the AI.
|
9 |
if (mStandStill) return; |
10 |
|
11 |
// Does the puck has an owner?
|
12 |
if (getPuckOwner() != null) { |
13 |
// Yeah, it has.
|
14 |
mBrain.popState(); |
15 |
|
16 |
if (doesMyTeamHaveThePuck()) { |
17 |
// My team just got the puck, it's attack time!
|
18 |
mBrain.pushState(attack); |
19 |
} else { |
20 |
// The opponent team got the puck, let's try to steal it.
|
21 |
mBrain.pushState(stealPuck); |
22 |
}
|
23 |
} else if (distance(this, aPuck) < 150) { |
24 |
// The puck has no owner and it is nearby. Let's pursue it.
|
25 |
mBrain.popState(); |
26 |
mBrain.pushState(pursuePuck); |
27 |
}
|
28 |
}
|
29 |
|
30 |
private function attack() :void { |
31 |
}
|
32 |
|
33 |
private function stealPuck() :void { |
34 |
}
|
35 |
|
36 |
private function pursuePuck() :void { |
37 |
}
|
38 |
}
|
Fahren wir mit der Implementierung der anderen Staaten fort.
Den Puck verfolgen
Nachdem der Athlet nun eine gewisse Wahrnehmung der Umgebung gewonnen hat und in der Lage ist, vom idle
in jeden Zustand zu wechseln, konzentrieren wir uns darauf, den Puck zu verfolgen, wenn er keinen Besitzer hat.
Ein Athlet wechselt sofort nach Beginn des Spiels zum pursuePuck
, da der Puck ohne Besitzer in der Mitte des Spielfelds platziert wird. Der pursuePuck
-Zustand hat drei Übergänge:



Der erste Übergang ist, dass puck is too far away
und versucht zu simulieren, was in einem echten Spiel beim Jagen des Pucks passiert. Aus strategischen Gründen versucht normalerweise der Athlet, der dem Puck am nächsten ist, ihn zu fangen, während die anderen warten oder versuchen zu helfen.
Ohne bei weit entferntem Puck in den idle
zu schalten, würde jeder KI-gesteuerte Athlet den Puck gleichzeitig verfolgen, auch wenn er sich von ihm entfernt befindet. Durch die Überprüfung der Entfernung zwischen dem Athleten und dem Puck löst sich pursuePuck
selbst aus dem Gehirn und drückt im idle
, wenn der Puck zu weit entfernt ist, was bedeutet, dass der Athlet einfach aufgegeben hat, den Puck zu verfolgen:
1 |
class Athlete { |
2 |
// (...)
|
3 |
|
4 |
private function pursuePuck() :void { |
5 |
var aPuck :Puck = getPuck(); |
6 |
|
7 |
if (distance(this, aPuck) > 150) { |
8 |
// Puck is too far away from our current position, so let's give up
|
9 |
// pursuing the puck and hope someone will be closer to get the puck
|
10 |
// for us.
|
11 |
mBrain.popState(); |
12 |
mBrain.pushState(idle); |
13 |
} else { |
14 |
// The puck is close, let's try to grab it.
|
15 |
}
|
16 |
}
|
17 |
|
18 |
// (...)
|
19 |
}
|
Wenn der Puck nah ist, muss der Athlet ihm nachgehen, was mit dem Suchverhalten leicht erreicht werden kann. Unter Verwendung der Position des Pucks als Suchziel verfolgt der Athlet den Puck anmutig und passt seine Flugbahn an, wenn sich der Puck bewegt:
1 |
class Athlete { |
2 |
// (...) |
3 |
private function pursuePuck() :void { |
4 |
var aPuck :Puck = getPuck(); |
5 |
|
6 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
7 |
|
8 |
if (distance(this, aPuck) > 150) { |
9 |
// Puck is too far away from our current position, so let's give up |
10 |
// pursuing the puck and hope someone will be closer to get the puck |
11 |
// for us. |
12 |
mBrain.popState(); |
13 |
mBrain.pushState(idle); |
14 |
} else { |
15 |
// The puck is close, let's try to grab it. |
16 |
if (aPuck.owner == null) { |
17 |
// Nobody has the puck, it's our chance to seek and get it! |
18 |
mBoid.steering = mBoid.steering + mBoid.seek(aPuck.position); |
19 |
|
20 |
} else { |
21 |
// Someone just got the puck. If the new puck owner belongs to my team, |
22 |
// we should switch to 'attack', otherwise I should switch to 'stealPuck' |
23 |
// and try to get the puck back. |
24 |
mBrain.popState(); |
25 |
mBrain.pushState(doesMyTeamHaveThePuck() ? attack : stealPuck); |
26 |
} |
27 |
} |
28 |
} |
29 |
} |
Die verbleibenden zwei Übergänge im pursuePuck
-Zustand, das team has the puck
und der opponent has the puck
, beziehen sich darauf, dass der Puck während des Verfolger-Prozesses gefangen wurde. Wenn jemand den Puck fängt, muss der Athlet den pursuePuck
-Zustand knacken und einen neuen ins Gehirn schieben.
Der zu pushende Zustand hängt vom Besitz des Pucks ab. Wenn der Aufruf von doesMyTeamHaveThePuck()
true
zurückgibt, bedeutet dies, dass ein Teamkollege den Puck bekommen hat, sodass der Athlet den attack
forcieren muss, was bedeutet, dass es an der Zeit ist, die Verfolgung des Pucks einzustellen und sich auf das Ziel des Gegners zuzubewegen. Wenn ein Gegner den Puck bekommen hat, muss der Athlet den stealPuck
drücken, wodurch das Team versucht, den Puck zurückzugewinnen.
Als kleine Verbesserung sollten Athleten während des pursuePuck
-Zustands nicht zu nahe beieinander bleiben, da eine "überfüllte" Verfolgungsbewegung unnatürlich ist. Das Hinzufügen einer Trennung zur Lenkungskraft des Staates (Zeile 6
im obigen Code) stellt sicher, dass die Athleten einen Mindestabstand zwischen ihnen einhalten.
Das Ergebnis ist ein Team, das den Puck verfolgen kann. Zu Testzwecken wird in dieser Demo der Puck alle paar Sekunden in der Mitte der Eisbahn platziert, damit sich die Athleten kontinuierlich bewegen:
Angriff mit dem Puck
Nach Erhalt des Pucks müssen sich ein Athlet und sein Team auf das gegnerische Tor zubewegen, um ein Tor zu erzielen. Das ist der Zweck des attack
-Zustands:



Der attack
-Zustand hat nur zwei Übergänge: opponent has the puck
und der puck has no owner
. Da der Zustand ausschließlich dazu dient, die Athleten zum gegnerischen Tor zu bewegen, macht es keinen Sinn, im Angriff zu bleiben, wenn der Puck nicht mehr im Besitz der Mannschaft ist.
Bezüglich der Bewegung zum gegnerischen Tor: Der Athlet, der den Puck trägt (Anführer) und die ihm helfenden Mitspieler sollten sich anders verhalten. Der Anführer muss das Ziel des Gegners erreichen und die Teamkollegen sollten ihm auf dem Weg helfen.
Dies kann implementiert werden, indem überprüft wird, ob der Athlet, der den Code ausführt, den Puck hat:
1 |
class Athlete { |
2 |
// (...)
|
3 |
private function attack() :void { |
4 |
var aPuckOwner :Athlete = getPuckOwner(); |
5 |
|
6 |
// Does the puck have an owner?
|
7 |
if (aPuckOwner != null) { |
8 |
// Yeah, it has. Let's find out if the owner belongs to the opponents team.
|
9 |
if (doesMyTeamHaveThePuck()) { |
10 |
if (amIThePuckOwner()) { |
11 |
// My team has the puck and I am the one who has it! Let's move
|
12 |
// towards the opponent's goal.
|
13 |
mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition()); |
14 |
|
15 |
} else { |
16 |
// My team has the puck, but a teammate has it. Let's just follow him
|
17 |
// to give some support during the attack.
|
18 |
mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid); |
19 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
20 |
}
|
21 |
} else { |
22 |
// The opponent has the puck! Stop the attack
|
23 |
// and try to steal it.
|
24 |
mBrain.popState(); |
25 |
mBrain.pushState(stealPuck); |
26 |
}
|
27 |
} else { |
28 |
// Puck has no owner, so there is no point to keep
|
29 |
// attacking. It's time to re-organize and start pursuing the puck.
|
30 |
mBrain.popState(); |
31 |
mBrain.pushState(pursuePuck); |
32 |
}
|
33 |
}
|
34 |
}
|
Wenn amIThePuckOwner()
true
zurückgibt (Zeile 10), hat der Athlet, der den Code ausführt, den Puck. In diesem Fall wird er nur die gegnerische Torposition suchen. Das ist so ziemlich die gleiche Logik, die verwendet wird, um den Puck im pursuePuck
-Zustand zu verfolgen.
Wenn amIThePuckOwner()
false
zurückgibt, hat der Athlet keinen Puck, daher muss er dem Anführer helfen. Dem Leiter zu helfen ist eine komplizierte Aufgabe, daher werden wir sie vereinfachen. Ein Athlet wird dem Führenden helfen, indem er einfach eine Position vor ihm sucht:



Wenn sich der Anführer bewegt, wird er von seinen Teamkollegen umgeben, die dem ahead
Punkt folgen. Dies gibt dem Anführer einige Optionen, an die er den Puck weitergeben kann, wenn es Probleme gibt. Wie in einem echten Spiel sollten auch die umliegenden Teamkollegen dem Führenden aus dem Weg gehen.
Dieses Unterstützungsmuster kann erreicht werden, indem eine leicht modifizierte Version des Führungsverhaltens hinzugefügt wird (Zeile 18). Der einzige Unterschied besteht darin, dass die Athleten einem Punkt vor dem Anführer folgen, anstatt einem Punkt hinter ihm, wie er ursprünglich in diesem Verhalten implementiert wurde.
Auch Athleten, die den Führenden unterstützen, sollten untereinander einen Mindestabstand einhalten. Dies wird durch Hinzufügen einer Trennkraft implementiert (Zeile 19).
Das Ergebnis ist eine Mannschaft, die sich auf das gegnerische Tor zubewegen kann, ohne sich zu drängen und eine unterstützte Angriffsbewegung zu simulieren:
Verbesserung der Angriffsunterstützung
Die aktuelle Implementierung des attack
-Zustands ist für einige Situationen gut genug, hat jedoch einen Fehler. Wenn jemand den Puck fängt, wird er zum Anführer und wird sofort von Mitspielern verfolgt.
Was passiert, wenn sich der Anführer seinem eigenen Ziel nähert, wenn er den Puck fängt? Schauen Sie sich die obige Demo genauer an und bemerken Sie das unnatürliche Muster, wenn Teamkollegen dem Anführer folgen.
Wenn der Anführer den Puck fängt, braucht das Suchverhalten einige Zeit, um die Flugbahn des Anführers zu korrigieren und ihn effektiv zum gegnerischen Tor zu bewegen. Selbst wenn der Anführer "manövriert", versuchen Teamkollegen, seinen ahead
zu suchen, was bedeutet, dass sie sich ihrem eigenen Ziel (oder dem Ort, auf den der Anführer starrt) nähern.
Wenn der Anführer endlich in Position ist und bereit ist, sich auf das gegnerische Tor zuzubewegen, werden die Teamkollegen "manövrieren", um dem Anführer zu folgen. Der Anführer bewegt sich dann ohne Unterstützung der Teamkollegen, solange die anderen ihre Flugbahnen anpassen.
Dieser Fehler kann behoben werden, indem überprüft wird, ob der Teamkollege dem Anführer voraus ist, wenn das Team den Puck zurückerlangt. Die Bedingung "vorne" bedeutet hier "näher am gegnerischen Tor":
1 |
class Athlete { |
2 |
// (...)
|
3 |
private function isAheadOfMe(theBoid :Boid) :Boolean { |
4 |
var aTargetDistance :Number = distance(getOpponentGoalPosition(), theBoid); |
5 |
var aMyDistance :Number = distance(getOpponentGoalPosition(), mBoid.position); |
6 |
|
7 |
return aTargetDistance <= aMyDistance; |
8 |
}
|
9 |
|
10 |
private function attack() :void { |
11 |
var aPuckOwner :Athlete = getPuckOwner(); |
12 |
|
13 |
// Does the puck have an owner?
|
14 |
if (aPuckOwner != null) { |
15 |
// Yeah, it has. Let's find out if the owner belongs to the opponents team.
|
16 |
if (doesMyTeamHaveThePuck()) { |
17 |
if (amIThePuckOwner()) { |
18 |
// My team has the puck and I am the one who has it! Let's move
|
19 |
// towards the opponent's goal.
|
20 |
mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition()); |
21 |
|
22 |
} else { |
23 |
// My team has the puck, but a teammate has it. Is he ahead of me?
|
24 |
if (isAheadOfMe(aPuckOwner.boid)) { |
25 |
// Yeah, he is ahead of me. Let's just follow him to give some support
|
26 |
// during the attack.
|
27 |
mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid); |
28 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
29 |
} else { |
30 |
// Nope, the teammate with the puck is behind me. In that case
|
31 |
// let's hold our current position with some separation from the
|
32 |
// other, so we prevent crowding.
|
33 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
34 |
}
|
35 |
}
|
36 |
} else { |
37 |
// The opponent has the puck! Stop the attack
|
38 |
// and try to steal it.
|
39 |
mBrain.popState(); |
40 |
mBrain.pushState(stealPuck); |
41 |
}
|
42 |
} else { |
43 |
// Puck has no owner, so there is no point to keep
|
44 |
// attacking. It's time to re-organize and start pursuing the puck.
|
45 |
mBrain.popState(); |
46 |
mBrain.pushState(pursuePuck); |
47 |
}
|
48 |
}
|
49 |
}
|
Wenn der Leader (der der Puckbesitzer ist) dem Athleten voraus ist, der den Code ausführt, dann sollte der Athlet dem Leader folgen, genau wie er es zuvor getan hat (Zeilen 27 und 28). Wenn der Führende hinter ihm ist, sollte der Athlet seine aktuelle Position halten und einen Mindestabstand zwischen den anderen einhalten (Linie 33).
Das Ergebnis ist etwas überzeugender als die erste attack
-Implementierung:
Tipp: Durch Optimieren der Entfernungsberechnungen und Vergleiche in der isAheadOfMe()
-Methode können Sie die Art und Weise ändern, in der Athleten ihre aktuellen Positionen halten.
Den Puck stehlen
Der letzte Zustand im Angriffsprozess ist der stealPuck
, der aktiv wird, wenn das gegnerische Team den Puck hat. Der Hauptzweck des stealPuck
-Zustands besteht darin, dem Gegner, der ihn trägt, den Puck zu stehlen, damit das Team wieder angreifen kann:



Da die Idee hinter diesem Zustand darin besteht, dem Gegner den Puck zu stehlen, wird sich stealPuck
selbst aus dem Gehirn platzen und den richtigen Zustand in den richtigen Zustand bringen, wenn der Puck vom Team zurückgeholt wird oder frei wird (dh er hat keinen Besitzer). Umgang mit der neuen Situation:
1 |
class Athlete { |
2 |
// (...)
|
3 |
private function stealPuck() :void { |
4 |
// Does the puck have any owner?
|
5 |
if (getPuckOwner() != null) { |
6 |
// Yeah, it has, but who has it?
|
7 |
if (doesMyTeamHaveThePuck()) { |
8 |
// My team has the puck, so it's time to stop trying to steal
|
9 |
// the puck and start attacking.
|
10 |
mBrain.popState(); |
11 |
mBrain.pushState(attack); |
12 |
} else { |
13 |
// An opponent has the puck.
|
14 |
var aOpponentLeader :Athlete = getPuckOwner(); |
15 |
|
16 |
// Let's pursue him while mantaining a certain separation from
|
17 |
// the others to avoid that everybody will ocuppy the same
|
18 |
// position in the pursuit.
|
19 |
mBoid.steering = mBoid.steering + mBoid.pursuit(aOpponentLeader.boid); |
20 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
21 |
}
|
22 |
} else { |
23 |
// The puck has no owner, it is probably running freely in the rink.
|
24 |
// There is no point to keep trying to steal it, so let's finish the 'stealPuck' state
|
25 |
// and switch to 'pursuePuck'.
|
26 |
mBrain.popState(); |
27 |
mBrain.pushState(pursuePuck); |
28 |
}
|
29 |
}
|
30 |
}
|
Wenn der Puck einen Besitzer hat und dieser zum gegnerischen Team gehört, muss der Athlet den gegnerischen Anführer verfolgen und versuchen, den Puck zu stehlen. Um den Anführer des Gegners zu verfolgen, muss ein Athlet vorhersagen, wo er in naher Zukunft sein wird, damit er in seiner Flugbahn abgefangen werden kann. Das ist etwas anderes, als nur den gegnerischen Anführer zu suchen.
Glücklicherweise kann dies mit dem Verfolger-Verhalten leicht erreicht werden (Zeile 19). Durch den Einsatz einer Verfolgungskraft im stealPuck
-Zustand versuchen die Athleten, den Anführer des Gegners abzufangen, anstatt ihm nur zu folgen:
Verhindern einer überfüllten Diebstahlbewegung
Die aktuelle Implementierung von stealPuck
funktioniert, aber in einem echten Spiel nähern sich nur ein oder zwei Athleten dem gegnerischen Anführer, um den Puck zu stehlen. Der Rest des Teams bleibt in den umliegenden Gebieten und versucht zu helfen, was ein überfülltes Diebstahlmuster verhindert.
Es kann behoben werden, indem vor der Verfolgung des gegnerischen Führers eine Distanzprüfung (Zeile 17) hinzugefügt wird:
1 |
class Athlete { |
2 |
// (...)
|
3 |
private function stealPuck() :void { |
4 |
// Does the puck have any owner?
|
5 |
if (getPuckOwner() != null) { |
6 |
// Yeah, it has, but who has it?
|
7 |
if (doesMyTeamHaveThePuck()) { |
8 |
// My team has the puck, so it's time to stop trying to steal
|
9 |
// the puck and start attacking.
|
10 |
mBrain.popState(); |
11 |
mBrain.pushState(attack); |
12 |
} else { |
13 |
// An opponent has the puck.
|
14 |
var aOpponentLeader :Athlete = getPuckOwner(); |
15 |
|
16 |
// Is the opponent with the puck close to me?
|
17 |
if (distance(aOpponentLeader, this) < 150) { |
18 |
// Yeah, he is close! Let's pursue him while mantaining a certain
|
19 |
// separation from the others to avoid that everybody will ocuppy the same
|
20 |
// position in the pursuit.
|
21 |
mBoid.steering = mBoid.steering.add(mBoid.pursuit(aOpponentLeader.boid)); |
22 |
mBoid.steering = mBoid.steering.add(mBoid.separation(50)); |
23 |
|
24 |
} else { |
25 |
// No, he is too far away. In the future, we will switch
|
26 |
// to 'defend' and hope someone closer to the puck can
|
27 |
// steal it for us.
|
28 |
// TODO: mBrain.popState();
|
29 |
// TODO: mBrain.pushState(defend);
|
30 |
}
|
31 |
}
|
32 |
} else { |
33 |
// The puck has no owner, it is probably running freely in the rink.
|
34 |
// There is no point to keep trying to steal it, so let's finish the 'stealPuck' state
|
35 |
// and switch to 'pursuePuck'.
|
36 |
mBrain.popState(); |
37 |
mBrain.pushState(pursuePuck); |
38 |
}
|
39 |
}
|
40 |
}
|
Anstatt den Anführer des Gegners blindlings zu verfolgen, prüft ein Athlet, ob der Abstand zwischen ihm und dem Anführer des Gegners kleiner als beispielsweise 150
ist. Wenn das true
, erfolgt die Verfolgung normal, aber wenn der Abstand größer als 150
ist, bedeutet dies, dass die Athlet ist zu weit vom gegnerischen Anführer entfernt.
In diesem Fall macht es keinen Sinn, weiterhin zu versuchen, den Puck zu stehlen, da er zu weit entfernt ist und wahrscheinlich bereits Teamkollegen vorhanden sind, die versuchen, dasselbe zu tun. Die beste Option ist, den stealPuck
aus dem Gehirn zu werfen und den defense
-Zustand zu pushen (was im nächsten Tutorial erklärt wird). Vorerst wird ein Athlet nur seine aktuelle Position halten, wenn der gegnerische Anführer zu weit entfernt ist.
Das Ergebnis ist ein überzeugenderes und natürlicheres Stealing-Muster (kein Crowding):
Gegner beim Angriff vermeiden
Es gibt noch einen letzten Trick, den die Athleten lernen müssen, um effektiv angreifen zu können. Im Moment bewegen sie sich auf das gegnerische Tor zu, ohne die Gegner auf dem Weg zu berücksichtigen. Ein Gegner muss als Bedrohung angesehen werden und sollte vermieden werden.
Mithilfe des Kollisionsvermeidungsverhaltens können Athleten Gegnern ausweichen, während sie sich bewegen:



Gegner werden als kreisförmige Hindernisse gesehen. Aufgrund der dynamischen Natur des Lenkverhaltens, das in jeder Spielschleife aktualisiert wird, funktioniert das Ausweichmuster elegant und reibungslos für sich bewegende Hindernisse (was hier der Fall ist).
Um die Athleten dazu zu bringen, Gegnern(Hindernissen) auszuweichen, muss dem Angriffszustand (Zeile 14) eine einzelne Zeile hinzugefügt werden:
1 |
class Athlete { |
2 |
// (...)
|
3 |
private function attack() :void { |
4 |
var aPuckOwner :Athlete = getPuckOwner(); |
5 |
|
6 |
// Does the puck have an owner?
|
7 |
if (aPuckOwner != null) { |
8 |
// Yeah, it has. Let's find out if the owner belongs to the opponents team.
|
9 |
if (doesMyTeamHaveThePuck()) { |
10 |
if (amIThePuckOwner()) { |
11 |
// My team has the puck and I am the one who has it! Let's move
|
12 |
// towards the opponent's goal, avoding any opponents along the way.
|
13 |
mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition()); |
14 |
mBoid.steering = mBoid.steering + mBoid.collisionAvoidance(getOpponentTeam().members); |
15 |
|
16 |
} else { |
17 |
// My team has the puck, but a teammate has it. Is he ahead of me?
|
18 |
if (isAheadOfMe(aPuckOwner.boid)) { |
19 |
// Yeah, he is ahead of me. Let's just follow him to give some support
|
20 |
// during the attack.
|
21 |
mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid); |
22 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
23 |
} else { |
24 |
// Nope, the teammate with the puck is behind me. In that case
|
25 |
// let's hold our current position with some separation from the
|
26 |
// other, so we prevent crowding.
|
27 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
28 |
}
|
29 |
}
|
30 |
} else { |
31 |
// The opponent has the puck! Stop the attack
|
32 |
// and try to steal it.
|
33 |
mBrain.popState(); |
34 |
mBrain.pushState(stealPuck); |
35 |
}
|
36 |
} else { |
37 |
// Puck has no owner, so there is no point to keep
|
38 |
// attacking. It's time to re-organize and start pursuing the puck.
|
39 |
mBrain.popState(); |
40 |
mBrain.pushState(pursuePuck); |
41 |
}
|
42 |
}
|
43 |
}
|
Diese Linie fügt dem Athleten eine Kollisionsvermeidungskraft hinzu, die mit den bereits vorhandenen Kräften kombiniert wird. Infolgedessen vermeidet der Athlet Hindernisse und sucht gleichzeitig das Ziel des Gegners.
Unten ist eine Demonstration eines Athleten, der den attack
-Zustand durchführt. Gegner sind unbeweglich, um das Kollisionsvermeidungsverhalten hervorzuheben:
Abschluss
In diesem Tutorial wurde die Implementierung des Angriffsmusters erklärt, mit dem die Athleten den Puck stehlen und zum gegnerischen Ziel tragen. Durch eine Kombination von Lenkverhalten sind Sportler nun in der Lage, komplexe Bewegungsmuster auszuführen, wie zum Beispiel einem Anführer zu folgen oder den Gegner mit dem Puck zu verfolgen.
Wie bereits erwähnt, zielt die Angriffsimplementierung darauf ab, zu simulieren, was Menschen tun, sodass das Ergebnis eine Annäherung an ein reales Spiel ist. Indem Sie die Zustände, aus denen sich der Angriff zusammensetzt, individuell anpassen, können Sie eine bessere Simulation erstellen oder eine, die Ihren Anforderungen entspricht.
Im nächsten Tutorial lernst du, wie man Athleten dazu bringt, sich zu verteidigen. Die KI wird vollständig funktionstüchtig, kann angreifen und verteidigen, was zu einem Spiel mit 100% KI-kontrollierten Teams führt, die gegeneinander spielen.
Verweise
- Sprite: Hockeystadion auf GraphicRiver
- Sprites: Hockeyspieler von Taylor J Glidden