Erstellen Sie eine Hockey-Spiel-KI mithilfe von Lenkverhalten: Grundlage
() translation by (you can also view the original English article)
Es gibt verschiedene Möglichkeiten, ein bestimmtes Spiel zu erstellen. Normalerweise wählt ein Entwickler etwas, das zu seinen Fähigkeiten passt, und verwendet dabei die Techniken, die er bereits kennt, um das bestmögliche Ergebnis zu erzielen. Manchmal wissen die Leute noch nicht, dass sie eine bestimmte Technik brauchen - vielleicht sogar eine einfachere und bessere -, einfach weil sie bereits wissen, wie man dieses Spiel erstellt.
In dieser Serie von Tutorials erfahren Sie, wie Sie künstliche Intelligenz für ein Eishockeyspiel mithilfe einer Kombination von Techniken erstellen, z. B. Lenkungsverhalten, die ich zuvor als Konzepte erklärt habe.
Hinweis: Obwohl dieses Tutorial mit AS3 und Flash geschrieben wurde, sollten Sie in der Lage sein, in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte zu verwenden.
Einführung
Hockey ist ein unterhaltsamer und beliebter Sport, und als Videospiel enthält es viele gamedev-Themen wie Bewegungsmuster, Teamwork (Angriff, Verteidigung), künstliche Intelligenz und Taktiken. Ein spielbares Hockeyspiel eignet sich hervorragend, um die Kombination einiger nützlicher Techniken zu demonstrieren.
Den Hockeymechaniker zu simulieren, bei dem Sportler laufen und sich bewegen, ist eine Herausforderung. Wenn die Bewegungsmuster selbst bei unterschiedlichen Pfaden vordefiniert sind, wird das Spiel vorhersehbar (und langweilig). Wie können wir eine solche dynamische Umgebung implementieren und gleichzeitig die Kontrolle über die Vorgänge behalten? Die Antwort lautet: Lenkverhalten verwenden.
Lenkverhalten soll mit improvisatorischer Navigation realistische Bewegungsmuster erzeugen. Sie basieren auf einfachen Kräften, die bei jedem Spielupdate kombiniert werden, und sind daher von Natur aus extrem dynamisch. Dies macht sie zur perfekten Wahl, um etwas so komplexes und dynamisches wie ein Hockey- oder Fußballspiel umzusetzen.
Umfang der Arbeit
Um Zeit und Unterricht zu sparen, reduzieren wir den Spielumfang etwas. Unser Hockeyspiel folgt nur einem kleinen Satz der ursprünglichen Regeln des Sports: In unserem Spiel gibt es keine Strafen und keine Torhüter, sodass sich jeder Athlet auf der Eisbahn bewegen kann:



Jedes Tor wird durch eine kleine "Mauer" ohne Netz ersetzt. Um zu punkten, muss ein Team den Puck (die Scheibe) bewegen, damit er eine Seite des gegnerischen Tores berührt. Wenn jemand punktet, organisieren sich beide Teams neu und der Puck wird in der Mitte platziert. Das Spiel wird einige Sekunden danach erneut gestartet.
Zum Puck-Handling: Wenn ein Athlet, sagen wir A, den Puck hat und von einem Gegner berührt wird, sagen Sie B, dann gewinnt B den Puck und A wird für einige Sekunden unbeweglich. Wenn der Puck die Eisbahn verlässt, wird er sofort in der Mitte der Eisbahn platziert.
Ich werde die Flixel-Spiel-Engine verwenden, um mich um den grafischen Teil des Codes zu kümmern. Der Engine-Code wird in den Beispielen jedoch vereinfacht oder weggelassen, um den Fokus auf das Spiel selbst zu behalten.
Die Umwelt gestalten
Beginnen wir mit der Spielumgebung, die aus einer Eisbahn, einer Anzahl von Athleten und zwei Toren besteht. Die Eisbahn besteht aus vier Rechtecken, die um die Eisfläche angeordnet sind. Diese Rechtecke kollidieren mit allem, was sie berührt, sodass nichts die Eisfläche verlassen wird.
Ein Athlet wird von der Athleten
klasse beschrieben:
1 |
public class Athlete |
2 |
{
|
3 |
private var mBoid :Boid; // controls the steering behavior stuff |
4 |
private var mId :int; // a unique identifier for the athelete |
5 |
|
6 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) { |
7 |
mBoid = new Boid(thePosX, thePosY, theTotalMass); |
8 |
}
|
9 |
|
10 |
public function update():void { |
11 |
// Clear all steering forces
|
12 |
mBoid.steering = null; |
13 |
|
14 |
// Wander around
|
15 |
wanderInTheRink(); |
16 |
|
17 |
// Update all steering stuff
|
18 |
mBoid.update(); |
19 |
}
|
20 |
|
21 |
private function wanderInTheRink() :void { |
22 |
var aRinkCenter :Vector3D = getRinkCenter(); |
23 |
|
24 |
// If the distance from the center is greater than 80,
|
25 |
// move back to the center, otherwise keep wandering.
|
26 |
if (Utils.distance(this, aRinkCenter) >= 80) { |
27 |
mBoid.steering = mBoid.steering + mBoid.seek(aRinkCenter); |
28 |
} else { |
29 |
mBoid.steering = mBoid.steering + mBoid.wander(); |
30 |
}
|
31 |
}
|
32 |
}
|
Die Eigenschaft mBoid
ist eine Instanz der Boid
-Klasse, eine Kapselung der mathematischen Logik, die in der Steuerungsverhaltensreihe verwendet wird. Die mBoid
-Instanz enthält unter anderem mathematische Vektoren, die die aktuelle Richtung, die Lenkkraft und die Position der Entität beschreiben.
Die update()
-Methode in der Athlete
-Klasse wird bei jeder Aktualisierung des Spiels aufgerufen. Für den Moment löscht es nur eine aktive Lenkkraft, fügt eine Wanderkraft hinzu und ruft schließlich mBoid.update()
auf. Mit dem vorherigen Befehl wird die gesamte in mBoid
eingeschlossene Steuerungsverhaltenslogik aktualisiert, sodass sich der Athlet bewegt (mithilfe der Euler-Integration).
Die Spielklasse, die für die Spielschleife verantwortlich ist, wird PlayState
genannt. Es hat die Eisbahn, zwei Gruppen von Athleten (eine Gruppe pro Team) und zwei Ziele:
1 |
public class PlayState |
2 |
{
|
3 |
private var mAthletes :FlxGroup; |
4 |
private var mRightGoal :Goal; |
5 |
private var mLeftGoal :Goal; |
6 |
|
7 |
public function create():void { |
8 |
// Here everything is created and added to the screen.
|
9 |
}
|
10 |
|
11 |
override public function update():void { |
12 |
// Make the rink collide with athletes
|
13 |
collide(mRink, mAthletes); |
14 |
|
15 |
// Ensure all athletes will remain inside the rink.
|
16 |
applyRinkContraints(); |
17 |
}
|
18 |
|
19 |
private function applyRinkContraints() :void { |
20 |
// check if athletes are within the rink
|
21 |
// boundaries.
|
22 |
}
|
23 |
}
|
Angenommen, ein einzelner Athlet wurde zum Match hinzugefügt, ist unten das Ergebnis von allem bisher:
Dem Mauszeiger folgen
Der Athlet muss dem Mauszeiger folgen, damit der Spieler tatsächlich etwas steuern kann. Da der Mauszeiger eine Position auf dem Bildschirm hat, kann er als Ziel für das Ankunftsverhalten verwendet werden.
Das Ankunftsverhalten bewirkt, dass ein Athlet die Cursorposition sucht, die Geschwindigkeit langsam verlangsamt, wenn er sich dem Cursor nähert, und stoppt schließlich dort.
In der Athlete
-Klasse ersetzen wir die Wandermethode durch das Ankunftsverhalten:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
|
5 |
public function update():void { |
6 |
// Clear all steering forces
|
7 |
mBoid.steering = null; |
8 |
|
9 |
// The athlete is controlled by the player,
|
10 |
// so just follow the mouse cursor.
|
11 |
followMouseCursor(); |
12 |
|
13 |
// Update all steering stuff
|
14 |
mBoid.update(); |
15 |
}
|
16 |
|
17 |
private function followMouseCursor() :void { |
18 |
var aMouse :Vector3D = getMouseCursorPosition(); |
19 |
mBoid.steering = mBoid.steering + mBoid.arrive(aMouse, 50); |
20 |
}
|
21 |
}
|
Das Ergebnis ist ein Athlet, der den Mauszeiger bewegen kann. Da die Bewegungslogik auf Lenkverhalten basiert, navigieren die Athleten auf der Bahn überzeugend und geschmeidig.
Verwenden Sie den Mauszeiger, um den Athleten in der folgenden Demo zu führen:
Hinzufügen und Steuern des Pucks
Der Puck wird durch die Klasse Puck
dargestellt. Die wichtigsten Teile sind die update()
- Methode und die mOwner
-Eigenschaft:
1 |
public class Puck |
2 |
{
|
3 |
public var velocity :Vector3D; |
4 |
public var position :Vector3D; |
5 |
private var mOwner :Athlete; // the athlete currently carrying the puck. |
6 |
|
7 |
public function setOwner(theOwner :Athlete) :void { |
8 |
if (mOwner != theOwner) { |
9 |
mOwner = theOwner; |
10 |
velocity = null; |
11 |
}
|
12 |
}
|
13 |
|
14 |
public function update():void { |
15 |
}
|
16 |
|
17 |
public function get owner() :Athlete { return mOwner; } |
18 |
}
|
Nach derselben Logik des Athleten wird die update()
-Methode des Pucks bei jeder Aktualisierung des Spiels aufgerufen. Die Eigenschaft mOwner
bestimmt, ob der Puck im Besitz eines Athleten ist. Wenn mOwner
den Wert null
hat, bedeutet dies, dass der Puck "frei" ist, und er wird sich bewegen und schließlich von den Spaziergängen der Eisbahn abprallen.
Wenn mOwner
nicht null
ist, bedeutet dies, dass der Puck von einem Athleten getragen wird. In diesem Fall werden Kollisionsprüfungen ignoriert und zwangsweise vor dem Athleten platziert. Dies kann mit dem velocity
vektor des Athleten erreicht werden, der auch der Richtung des Athleten entspricht:



Der Ahead
-Vektor ist eine Kopie des velocity
vektors des Athleten und zeigt in dieselbe Richtung. Nachdem sich der ahead
normalisiert hat, kann er um einen beliebigen Wert (z. B. 30
) skaliert werden, um zu steuern, wie weit der Puck vor dem Sportler platziert wird.
Schließlich erhält die Position
des Pucks die Position
des Athleten, die nach ahead
hinzugefügt wird, und platziert den Puck an der gewünschten Position.
Unten ist der Code für all das:
1 |
public class Puck |
2 |
{
|
3 |
// (...)
|
4 |
|
5 |
private function placeAheadOfOwner() :void { |
6 |
var ahead :Vector3D = mOwner.boid.velocity.clone(); |
7 |
|
8 |
ahead = normalize(ahead) * 30; |
9 |
position = mOwner.boid.position + ahead; |
10 |
}
|
11 |
|
12 |
override public function update():void { |
13 |
if (mOwner != null) { |
14 |
placeAheadOfOwner(); |
15 |
}
|
16 |
}
|
17 |
|
18 |
// (...)
|
19 |
}
|
In der PlayState
-Klasse gibt es einen Kollisionstest, um zu prüfen, ob der Puck einen Athleten überlappt. Wenn dies der Fall ist, wird der Athlet, der gerade den Puck berührt hat, sein neuer Besitzer. Das Ergebnis ist ein Puck, der beim Sportler "haftet". Führen Sie den Athleten in der folgenden Demo dazu, den Puck in der Mitte der Eisbahn zu berühren, um dies in Aktion zu sehen:
Den Puck schlagen
Es ist Zeit, den Puck zu bewegen, wenn er vom Stock getroffen wird. Unabhängig vom Athleten, der den Puck trägt, ist zur Simulation eines Schlags mit dem Stock nur die Berechnung eines neuen Geschwindigkeitsvektors erforderlich. Diese neue Geschwindigkeit bewegt den Puck in Richtung des gewünschten Ziels.
Ein Geschwindigkeitsvektor kann durch einen Positionsvektor von einem anderen erzeugt werden; Der neu erzeugte Vektor bewegt sich dann von einer Position zur anderen. Genau das ist nötig, um den neuen Geschwindigkeitsvektor des Pucks nach einem Treffer zu berechnen:



In der Abbildung oben ist der Zielpunkt der Mauszeiger. Die aktuelle Position des Pucks kann als Ausgangspunkt verwendet werden, während der Punkt, an dem sich der Puck befinden soll, nachdem er vom Stock getroffen wurde, als Endpunkt verwendet werden kann.
Der folgende Pseudocode zeigt die Implementierung von goFromStickHit()
, einer Methode in der Puck
-Klasse, die die im obigen Bild dargestellte Logik implementiert:
1 |
public class Puck |
2 |
{
|
3 |
// (...)
|
4 |
|
5 |
public function goFromStickHit(theAthlete :Athlete, theDestination :Vector3D, theSpeed :Number = 160) :void { |
6 |
// Place the puck ahead of the owner to prevent unexpected trajectories
|
7 |
// (e.g. puck colliding the athlete that just hit it)
|
8 |
placeAheadOfOwner(); |
9 |
|
10 |
// Mark the puck as free (no owner)
|
11 |
setOwner(null); |
12 |
|
13 |
// Calculate the puck's new velocity
|
14 |
var new_velocity :Vector3D = theDestination - position; |
15 |
velocity = normalize(new_velocity) * theSpeed; |
16 |
}
|
17 |
}
|
Der new_velocity
-Vektor bewegt sich von der aktuellen Position des Pucks zum Ziel (theDestination
). Danach wird es von theSpeed
normalisiert und skaliert, wodurch die Größe (Länge) von new_velocity
definiert wird. Diese Operation definiert mit anderen Worten, wie schnell sich der Puck von seiner aktuellen Position zum Ziel bewegt. Schließlich wird der velocity
vektor des Pucks durch new_velocity
ersetzt.
In der PlayState
-Klasse wird die goFromStichHit()
- Methode jedes Mal aufgerufen, wenn der Player auf den Bildschirm klickt. In diesem Fall wird der Mauszeiger als Ziel für den Treffer verwendet. Das Ergebnis ist in dieser Demo zu sehen:
Hinzufügen des A.I.
Bis jetzt hatten wir nur einen einzigen Sportler, der sich auf der Eisbahn bewegte. Wenn mehr Athleten hinzugefügt werden, muss die KI implementiert werden, damit alle diese Athleten so aussehen, als würden sie lebendig und denkend.
Um dies zu erreichen, verwenden wir eine stapelbasierte Zustandsmaschine (Stack-based FSM, kurz: FSM). Wie zuvor beschrieben, sind FSMs vielseitig und nützlich für die Implementierung von KI in Spielen.
Für unser Hockeyspiel wird der Athlete
-Klasse eine Eigenschaft namens mBrain
hinzugefügt:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
private var mBrain :StackFSM; // controls the AI stuff |
5 |
|
6 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) { |
7 |
// (...)
|
8 |
mBrain = new StackFSM(); |
9 |
}
|
10 |
|
11 |
// (...)
|
12 |
}
|
Diese Eigenschaft ist eine Instanz von StackFSM
, einer zuvor im FSM-Lernprogramm verwendeten Klasse. Es verwendet einen Stack, um die AI-Zustände einer Entität zu steuern. Jeder Staat wird als Methode beschrieben; Wenn ein Status in den Stack verschoben wird, wird er zur aktiven Methode und wird bei jeder Aktualisierung des Spiels aufgerufen.
Jeder Staat führt eine bestimmte Aufgabe aus, beispielsweise den Athleten in Richtung Puck zu bewegen. Jeder Staat ist dafür verantwortlich, sich selbst zu beenden, das heißt, er ist dafür verantwortlich, sich vom Stapel zu befreien.
Der Athlet kann jetzt vom Spieler oder von der KI gesteuert werden. Daher muss die update()
- Methode in der Athlet
-Klasse geändert werden, um diese Situation zu überprüfen:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
|
5 |
public function update():void { |
6 |
// Clear all steering forces
|
7 |
mBoid.steering = null; |
8 |
|
9 |
if (mControlledByAI) { |
10 |
// The athlete is controlled by the AI. Update the brain (FSM) and
|
11 |
// stay away from rink walls.
|
12 |
mBrain.update(); |
13 |
|
14 |
} else { |
15 |
// The athlete is controlled by the player, so just follow
|
16 |
// the mouse cursor.
|
17 |
followMouseCursor(); |
18 |
}
|
19 |
|
20 |
// Update all steering stuff
|
21 |
mBoid.update(); |
22 |
}
|
23 |
}
|
Wenn die KI aktiv ist, wird mBrain
aktualisiert, wodurch die derzeit aktive Statusmethode aufgerufen wird, wodurch sich der Athlet entsprechend verhält. Wenn der Spieler die Kontrolle hat, wird mBrain
alle ignoriert und der Athlet bewegt sich gemäß den Anweisungen des Spielers.
Was die Zustände betrifft, die in das Gehirn vordringen sollen: Lassen Sie uns jetzt nur zwei davon implementieren. Ein Staat lässt sich von einem Athleten auf ein Match vorbereiten; Bei der Vorbereitung auf das Spiel bewegt sich ein Athlet zu seiner Position in der Halle und bleibt stehen und starrt den Puck an. Der andere Staat wird den Athleten dazu bringen, einfach stillzustehen und den Puck anzustarren.
In den nächsten Abschnitten implementieren wir diese Zustände.
Der untätige Zustand
Wenn sich der Athlet im idle
stand befindet, wird er aufhören sich zu bewegen und den Puck anstarren. Dieser Status wird verwendet, wenn der Athlet sich bereits in der Eisbahn befindet und darauf wartet, dass etwas passiert, beispielsweise der Beginn des Spiels.
Der Zustand wird in der Athlete
-Klasse unter der Methode idle()
codiert:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number, theTeam :FlxGroup) { |
5 |
// (...)
|
6 |
|
7 |
// Tell the brain the current state is 'idle'
|
8 |
mBrain.pushState(idle); |
9 |
}
|
10 |
|
11 |
private function idle() :void { |
12 |
var aPuck :Puck = getPuck(); |
13 |
stopAndlookAt(aPuck.position); |
14 |
}
|
15 |
|
16 |
private function stopAndlookAt(thePoint :Vector3D) :void { |
17 |
mBoid.velocity = thePoint - mBoid.position; |
18 |
mBoid.velocity = normalize(mBoid.velocity) * 0.01; |
19 |
}
|
20 |
}
|
Da sich diese Methode nicht vom Stapel löst, bleibt sie für immer aktiv. In der Zukunft wird dieser Staat sich selbst platzieren, um Platz für andere Staaten wie Angriff zu schaffen, aber im Moment macht es den Trick.
Die Methode stopAndStareAt()
folgt demselben Prinzip, das zur Berechnung der Puck-Geschwindigkeit nach einem Treffer verwendet wird. Ein Vektor von der Position des Athleten zur Position des Pucks wird von der Point - mBoid.position
berechnet und als neuer Geschwindigkeitsvektor des Athleten verwendet.
Dieser neue Geschwindigkeitsvektor bewegt den Athleten in Richtung Puck. Um sicherzustellen, dass sich der Athlet nicht bewegt, wird der Vektor um 0,01
skaliert, wodurch seine Länge auf nahezu Null "schrumpft". Der Sportler hört auf, sich zu bewegen, hält den Puck aber weiter an.
Für ein Match vorbereiten
Befindet sich der Athlet im Status prepareForMatch
, bewegt er sich in Richtung seiner Ausgangsposition und bleibt dort glatt stehen. In der Ausgangsposition sollte sich der Athlet kurz vor Beginn des Spiels befinden. Da der Athlet am Ziel anhalten sollte, kann das Ankunftsverhalten wieder verwendet werden:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
private var mInitialPosition :Vector3D; // the position in the rink where the athlete should be placed |
5 |
|
6 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number, theTeam :FlxGroup) { |
7 |
// (...)
|
8 |
mInitialPosition = new Vector3D(thePosX, thePosY); |
9 |
|
10 |
// Tell the brain the current state is 'idle'
|
11 |
mBrain.pushState(idle); |
12 |
}
|
13 |
|
14 |
private function prepareForMatch() :void { |
15 |
mBoid.steering = mBoid.steering + mBoid.arrive(mInitialPosition, 80); |
16 |
|
17 |
// Am I at the initial position?
|
18 |
if (distance(mBoid.position, mInitialPosition) <= 5) { |
19 |
// I'm in position, time to stare at the puck.
|
20 |
mBrain.popState(); |
21 |
mBrain.pushState(idle); |
22 |
}
|
23 |
}
|
24 |
|
25 |
// (...)
|
26 |
}
|
Der Staat verwendet das Ankunftsverhalten, um den Athleten zur Ausgangsposition zu bewegen. Wenn der Abstand zwischen dem Athleten und seiner Ausgangsposition weniger als 5
beträgt, bedeutet dies, dass der Athlet am gewünschten Ort angekommen ist. Wenn dies der Fall ist, wird sich prepareForMatch
vom Stapel entfernt und in den idle
stand versetzt, wodurch es zum neuen aktiven Status wird.
Im Folgenden wird das Ergebnis der Verwendung eines stapelbasierten FSM zur Steuerung mehrerer Athleten beschrieben. Drücken Sie G
, um sie an zufälligen Positionen in der Eisbahn zu prepareForMatch
platzieren.
Fazit
In diesem Lernprogramm wurden die Grundlagen für die Implementierung eines Hockeyspiels mit Steuerungsverhalten und stapelbasierten Zustandsautomaten vorgestellt. Mit einer Kombination dieser Konzepte kann sich ein Athlet auf dem Platz bewegen, indem er dem Mauszeiger folgt. Der Athlet kann den Puck auch auf ein Ziel treffen.
Mit zwei Zuständen und einem stapelbasierten FSM können sich die Athleten neu organisieren und ihre Position in der Eisbahn vorbereiten, um sich auf das Spiel vorzubereiten.
Im nächsten Tutorial lernen Sie, wie man die Athleten angreift, indem sie den Puck zum Ziel tragen und dabei den Gegner meiden.
Verweise
- Sprite: Hockey Stadium auf GraphicRiver
- Sprites: Hockey-Spieler von Taylor J Glidden