Advertisement
  1. Game Development
  2. Artificial Intelligence

Erstellen Sie eine Hockeyspiel-KI mit Lenkverhalten: SpielmechanikMechan

Scroll to top
Read Time: 13 min
This post is part of a series called Create AI for a Hockey Game Using Steering Behaviors.
Create a Hockey Game AI Using Steering Behaviors: Defense

() translation by (you can also view the original English article)

In früheren Beiträgen dieser Serie haben wir uns auf die Konzepte hinter der künstlichen Intelligenz konzentriert, über die wir gelernt haben. In diesem Teil werden wir die gesamte Implementierung in ein vollständig spielbares Hockeyspiel packen. Sie erfahren, wie Sie die fehlenden Teile hinzufügen, die erforderlich sind, um daraus ein Spiel zu machen, z. B. Punktestand, Power-Ups und ein bisschen Spieldesign.

Endergebnis

Unten ist das Spiel, das mit allen in diesem Tutorial beschriebenen Elementen implementiert wird.

Thinking Game Design

Die vorherigen Teile dieser Serie konzentrierten sich darauf, zu erklären, wie die Spiel-KI funktioniert. Jeder Teil detailliert einen bestimmten Aspekt des Spiels, wie zum Beispiel, wie sich die Athleten bewegen und wie Angriff und Verteidigung implementiert werden. Sie basierten auf Konzepten wie Steuerungsverhalten und stapelbasierten endlichen Zustandsautomaten.

Um ein vollständig spielbares Spiel zu erstellen, müssen jedoch all diese Aspekte in eine grundlegende Spielmechanik eingebettet werden. Die naheliegendste Wahl wäre, alle offiziellen Regeln eines offiziellen Hockeyspiels umzusetzen, aber das würde viel Arbeit und Zeit erfordern. Nehmen wir stattdessen einen einfacheren Fantasy-Ansatz.

Alle Hockeyregeln werden durch eine einzige ersetzt: Wer den Puck trägt und von einem Gegner berührt wird, friert ein und zerspringt in Millionen Stück! Dadurch wird das Spiel einfacher zu spielen und macht beiden Spielern Spaß: demjenigen, der den Puck trägt, und dem anderen, der versucht, ihn zurückzugewinnen.

Um diese Mechanik zu verbessern, fügen wir ein paar Power-Ups hinzu. Sie helfen dem Spieler, ein Tor zu erzielen und das Spiel etwas dynamischer zu machen.

Hinzufügen der Fähigkeit zum Scoren

Beginnen wir mit dem Punktesystem, das dafür verantwortlich ist, zu bestimmen, wer gewinnt oder verliert. Ein Team erzielt jedes Mal ein Tor, wenn der Puck in das gegnerische Tor eindringt.

Der einfachste Weg, dies zu implementieren, ist die Verwendung von zwei überlappenden Rechtecken:

Overlapped rectangles describing the goal area If the puck collides with the red rectangle the team scoresOverlapped rectangles describing the goal area If the puck collides with the red rectangle the team scoresOverlapped rectangles describing the goal area If the puck collides with the red rectangle the team scores
Überlappte Rechtecke, die den Zielbereich beschreiben. Wenn der Puck mit dem roten Rechteck kollidiert, punktet das Team.

Das grüne Rechteck stellt die Fläche dar, die von der Torstruktur (dem Rahmen und dem Netz) eingenommen wird. Es funktioniert wie ein fester Block, so dass der Puck und die Athleten sich nicht hindurch bewegen können; sie werden zurückspringen.

Das rote Rechteck stellt den "Score-Bereich" dar. Wenn der Puck dieses Rechteck überlappt, bedeutet dies, dass eine Mannschaft gerade ein Tor erzielt hat.

Das rote Rechteck ist kleiner als das grüne und wird davor platziert. Wenn der Puck also das Tor auf einer anderen Seite als der Vorderseite berührt, prallt er zurück und es wird keine Punktzahl hinzugefügt:

A few examples of how the puck would behave if it touched the rectangles while movingA few examples of how the puck would behave if it touched the rectangles while movingA few examples of how the puck would behave if it touched the rectangles while moving
Einige Beispiele dafür, wie sich der Puck verhalten würde, wenn er während der Bewegung die Rechtecke berührt.

Alles organisieren, nachdem jemand punktet

Nachdem ein Team gewertet hat, müssen alle Athleten in ihre Ausgangsposition zurückkehren und der Puck muss wieder in der Spielfeldmitte platziert werden. Nach diesem Vorgang kann das Spiel fortgesetzt werden.

Sportler in ihre Ausgangsposition bringen

Wie im ersten Teil dieser Serie erklärt, haben alle Athleten einen KI-Zustand namens PrepareForMatch, der sie in die Ausgangsposition bewegt und dort sanft zum Stehen bringt.

Wenn der Puck einen der "Score-Bereiche" überlappt, wird jeder derzeit aktive KI-Zustand aller Athleten entfernt und prepareForMatch ins Gehirn geschoben. Wo immer die Athleten sind, kehren sie nach wenigen Sekunden in ihre Ausgangsposition zurück:

Bewegen der Kamera in Richtung des Eisbahnzentrums

Da die Kamera immer dem Puck folgt, ändert sich die aktuelle Ansicht schlagartig, wenn sie nach einem Tor direkt ins Eisfeldzentrum teleportiert wird, was hässlich und verwirrend wäre.

Ein besserer Weg, dies zu tun, besteht darin, den Puck sanft in Richtung der Spielfeldmitte zu bewegen; Da die Kamera dem Puck folgt, wird der Blick vom Ziel zur Eisbahnmitte elegant verschoben.

Dies kann erreicht werden, indem der Geschwindigkeitsvektor des Pucks geändert wird, nachdem er einen beliebigen Zielbereich getroffen hat. Der neue Geschwindigkeitsvektor muss den Puck in Richtung Eisbahnmitte "schieben", damit er wie folgt berechnet werden kann:

1
var c :Vector3D = getRinkCenter();
2
var p :Vector3D = puck.position;
3
4
var v :Vector3D = c - p;
5
v = normalize(v) * 100;
6
7
puck.velocity = v;

Durch Subtrahieren der Position des Spielfeldzentrums von der aktuellen Position des Pucks ist es möglich, einen Vektor zu berechnen, der direkt auf das Spielfeldzentrum zeigt.

Nach der Normierung dieses Vektors kann er um einen beliebigen Wert skaliert werden, z. B. 100, der steuert, wie schnell sich der Puck in Richtung der Spielfeldmitte bewegt.

Unten ist ein Bild mit einer Darstellung des neuen Geschwindigkeitsvektors:

Calculation of a new velocity vector that will move the puck towards the rink centerCalculation of a new velocity vector that will move the puck towards the rink centerCalculation of a new velocity vector that will move the puck towards the rink center
Berechnung eines neuen Geschwindigkeitsvektors, der den Puck in Richtung der Spielfeldmitte bewegt.

Dieser Vektor V wird als Geschwindigkeitsvektor des Pucks verwendet, sodass sich der Puck wie beabsichtigt in Richtung der Spielfeldmitte bewegt.

Um ein seltsames Verhalten während der Bewegung des Pucks in Richtung des Spielfeldzentrums zu verhindern, wie z. B. eine Interaktion mit einem Athleten, wird der Puck während des Vorgangs deaktiviert. Als Konsequenz hört es auf, mit Sportlern zu interagieren und wird als unsichtbar markiert. Der Spieler sieht nicht, wie sich der Puck bewegt, aber die Kamera folgt ihm trotzdem.

Um zu entscheiden, ob der Puck bereits in Position ist, wird während der Bewegung der Abstand zwischen ihm und der Spielfeldmitte berechnet. Ist er beispielsweise kleiner als 10, ist der Puck nahe genug, um direkt in der Spielfeldmitte platziert und reaktiviert zu werden, damit das Spiel fortgesetzt werden kann.

Power-Ups hinzufügen

Die Idee hinter Power-Ups besteht darin, dem Spieler zu helfen, das Hauptziel des Spiels zu erreichen, nämlich zu punkten, indem er den Puck zum gegnerischen Tor trägt.

Aus Gründen des Umfangs wird unser Spiel nur zwei Power-Ups haben: Ghost Help und Fear The Puck. Ersteres erweitert das Team des Spielers für einige Zeit um drei zusätzliche Athleten, während letzteres die Gegner für einige Sekunden vor dem Puck fliehen lässt.

Power-Ups werden beiden Teams hinzugefügt, wenn jemand ein Tor erzielt.

Implementieren des "Ghost Help"-Power-ups

Da alle durch das Ghost Help-Power-Up hinzugefügten Athleten vorübergehend sind, muss die Athlete-Klasse geändert werden, damit ein Athlet als "Geister" markiert werden kann. Wenn ein Athlet ein Geist ist, wird er sich nach einigen Sekunden aus dem Spiel entfernen.

Unten ist die Athlete-Klasse, die nur die Ergänzungen hervorhebt, die vorgenommen wurden, um die Ghost-Funktionalität zu berücksichtigen:

1
public class Athlete
2
{
3
    // (...)

4
	private var mGhost :Boolean;        // tells if the athlete is a ghost (a powerup that adds new athletes to help steal the puck).

5
	private var mGhostCounter :Number;  // counts the time a ghost will remain active

6
	
7
	public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
8
		// (...)

9
		mGhost = false;
10
		mGhostCounter = 0;
11
		
12
		// (...)

13
	}
14
    
15
    public function setGhost(theStatus :Boolean, theDuration :Number) :void {
16
		mGhost = theStatus;
17
		mGhostCounter = theDuration;
18
	}
19
	
20
	public function amIAGhost() :Boolean {
21
		return mGhost;
22
	}
23
	
24
	public function update() :void {
25
		// (...)

26
		
27
		// Update powerup counters and stuff

28
		updatePowerups();
29
		
30
		// (...)

31
	}
32
    
33
    public function updatePowerups() :void {
34
        // TODO.

35
    }
36
}

Die Eigenschaft mGhost ist ein boolescher Wert, der angibt, ob der Athlet ein Geist ist oder nicht, während mGhostCounter die Anzahl der Sekunden enthält, die der Athlet warten soll, bevor er sich aus dem Spiel entfernt.

Diese beiden Eigenschaften werden von der Methode updatePowerups() verwendet:

1
private function updatePowerups():void {		
2
	// If the athlete is a ghost, it has a counter that controls

3
	// when it must be removed.

4
	if (amIAGhost()) {
5
		mGhostCounter -= time_elapsed;
6
		
7
		if (mGhostCounter <= 2) {
8
			// Make athlete flicker when it is about to be removed.

9
			flicker(0.5);
10
		}
11
		
12
		if (mGhostCounter <= 0) {
13
			// Time to leave this world! (again)

14
			kill();
15
		}
16
	}
17
}

Die updatePowerups()-Methode, die innerhalb der update()-Routine des Athleten aufgerufen wird, verarbeitet die gesamte Power-Up-Verarbeitung im Athleten. Im Moment prüft er nur, ob der aktuelle Athlet ein Geist ist oder nicht. Wenn dies der Fall ist, wird die mGhostCounter-Eigenschaft um die seit der letzten Aktualisierung verstrichene Zeit verringert.

Wenn der Wert von mGhostCounter Null erreicht, bedeutet dies, dass der temporäre Athlet lange genug aktiv war und sich daher aus dem Spiel entfernen muss. Um den Spieler darauf aufmerksam zu machen, beginnt der Athlet seine letzten zwei Sekunden zu flackern, bevor er verschwindet.

Schließlich ist es an der Zeit, den Prozess des Hinzufügens der temporären Athleten zu implementieren, wenn das Power-Up aktiviert wird. Dies wird in der Methode powerupGhostHelp() durchgeführt, die in der Hauptspiellogik verfügbar ist:

1
private function powerupGhostHelp() :void {
2
	var aAthlete :Athlete;	
3
	
4
	for (var i:int = 0; i < 3; i++) {
5
        // Add the new athlete to the list of athletes

6
		aAthlete = addAthlete(RINK_WIDTH / 2, RINK_HEIGHT - 100);
7
        
8
        // Mark the athlete as a ghost which will be removed after 10 seconds.

9
		aAthlete.setGhost(true, 10);
10
	}
11
}

Diese Methode durchläuft eine Schleife, die der Anzahl der temporären Athleten entspricht, die hinzugefügt werden. Jeder neue Athlet wird am unteren Rand der Eisbahn hinzugefügt und als Geist markiert.

Wie zuvor beschrieben, entfernen sich Geisterathleten aus dem Spiel.

Implementieren des Power-Ups "Fear The Puck"

Das Fear The Puck Power-Up lässt alle Gegner für einige Sekunden vor dem Puck fliehen.

Genau wie das Ghost Help-Power-Up muss die Athlete-Klasse geändert werden, um diese Funktionalität zu berücksichtigen:

1
public class Athlete
2
{
3
    // (...)

4
    private var mFearCounter :Number; // counts the time the athlete should evade from puck (when fear powerup is active).

5
6
	
7
	public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
8
		// (...)

9
		mFearCounter = 0;
10
		
11
		// (...)

12
	}
13
    
14
	public function fearPuck(theDuration: Number = 2) :void {
15
		mFearCounter = theDuration;
16
	}
17
	
18
    // Returns true if the mFearCounter has a value and the athlete

19
    // is not idle or preparing for a match.

20
	private function shouldIEvadeFromPuck() :Boolean {
21
		return mFearCounter > 0 && mBrain.getCurrentState() != idle && mBrain.getCurrentState() != prepareForMatch;
22
	}
23
    
24
    private function updatePowerups():void {
25
    	if(mFearCounter > 0) {
26
			mFearCounter -= elapsed_time;
27
		}
28
		
29
		// (...)

30
	}
31
	
32
	public function update() :void {
33
		// (...)

34
		
35
		// Update powerup counters and stuff

36
		updatePowerups();
37
        
38
		// If the athlete is an AI-controlled opponent

39
		if (amIAnAiControlledOpponent()) {
40
			// Check if "fear of the puck" power-up is active.

41
            // If that's true, evade from puck.

42
			if(shouldIEvadeFromPuck()) {
43
				evadeFromPuck();
44
			}
45
        }			
46
		
47
		// (...)

48
	}
49
    
50
    public function evadeFromPuck() :void {
51
        // TODO

52
    }
53
}

Zuerst wird die Methode updatePowerups() geändert, um die mFearCounter-Eigenschaft zu verringern, die die Zeit enthält, die der Athlet dem Puck ausweichen sollte. Die mFearCounter-Eigenschaft wird jedes Mal geändert, wenn die Methode fearPuck() aufgerufen wird.

In der Methode update() des Athleten wird ein Test hinzugefügt, um zu überprüfen, ob das Einschalten erfolgen soll. Wenn der Athlet ein von der KI kontrollierter Gegner ist (amIAnAiControlledOpponent() gibt true zurück) und der Athlet dem Puck ausweichen sollte (shouldIEvadeFromPuck() gibt ebenfalls true zurück), wird die Methode evadeFromPuck() aufgerufen.

Die Methode evadeFromPuck() verwendet das Ausweichverhalten, das eine Entität dazu bringt, jedes Objekt und seine Flugbahn insgesamt zu vermeiden:

1
private function evadeFromPuck() :void {
2
	mBoid.steering = mBoid.steering + mBoid.evade(getPuck().getBoid());
3
}

Die Methode evadeFromPuck() fügt lediglich eine Ausweichkraft zur Lenkkraft des aktuellen Athleten hinzu. Dadurch weicht er dem Puck aus, ohne die bereits hinzugefügten Lenkkräfte zu ignorieren, wie sie beispielsweise durch den derzeit aktiven KI-Zustand erzeugt werden.

Um ausweichbar zu sein, muss sich der Puck wie alle Athleten wie ein Boid verhalten (weitere Informationen dazu im ersten Teil der Serie). Als Konsequenz muss der Puck-Klasse eine boid-Eigenschaft hinzugefügt werden, die die aktuelle Position und Geschwindigkeit des Pucks enthält:

1
class Puck {
2
    // (...)

3
    private var mBoid :Boid;
4
    
5
    // (...)

6
    
7
    public function update() {
8
        // (...)

9
        mBoid.update();
10
    }
11
    
12
    public function getBoid() :Boid {
13
        return mBoid;
14
    }
15
    
16
    // (...)

17
}

Schließlich aktualisieren wir die Hauptspiellogik, damit die Gegner den Puck fürchten, wenn das Power-Up aktiviert wird:

1
private function powerupFearPuck() :void {
2
	var i           :uint,
3
		athletes    :Array  = rightTeam.members,
4
		size        :uint   = athletes.length;
5
			
6
	for (i = 0; i < size; i++) {
7
		if (athletes[i] != null) {
8
            // Make athlete fear the puck for 3 seconds.

9
			athletes[i].fearPuck(3);
10
		}
11
	}
12
}

Die Methode iteriert über alle gegnerischen Athleten (in diesem Fall das richtige Team) und ruft bei jedem von ihnen die Methode fearkPuck() auf. Dies wird die Logik auslösen, die die Athleten, wie zuvor erklärt, für einige Sekunden vor dem Puck fürchten lässt.

Einfrieren und Zertrümmern

Die letzte Ergänzung des Spiels ist der einfrierende und erschütternde Teil. Es wird in der Hauptspiellogik durchgeführt, wo eine Routine überprüft, ob sich die Athleten des linken Teams mit den Athleten des rechten Teams überschneiden.

Diese Überschneidungsprüfung wird automatisch von der Flixel-Spiel-Engine durchgeführt, die jedes Mal, wenn eine Überschneidung gefunden wird, einen Rückruf auslöst:

1
private function athletesOverlapped(theLeftAthlete :Athlete, theRightAthlete :Athlete) :void {
2
    // Does the puck have an owner?	

3
	if (mPuck.owner != null) {
4
        // Yes, it does.

5
		if (mPuck.owner == theLeftAthlete) {
6
            //Puck's owner is the left athlete

7
			theLeftAthlete.shatter();
8
			mPuck.setOwner(theRightAthlete);
9
10
		} else if (mPuck.owner == theRightAthlete) {
11
            //Puck's owner is the right athlete

12
			theRightAthlete.shatter();
13
			mPuck.setOwner(theLeftAthlete);
14
		}
15
	}
16
}

Dieser Callback erhält als Parameter die Athleten jedes Teams, die sich überlappten. Ein Test prüft, ob der Besitzer des Pucks nicht null ist, was bedeutet, dass er von jemandem getragen wird.

In diesem Fall wird der Besitzer des Pucks mit den Athleten verglichen, die sich gerade überlappten. Wenn einer von ihnen den Puck trägt (er ist also der Besitzer des Pucks), ist er erschüttert und der Besitz des Pucks geht auf den anderen Athleten über.

Die shatter()-Methode in der Athlete-Klasse markiert den Athleten als inaktiv und platziert ihn nach einigen Sekunden am unteren Rand der Eisbahn. Es wird auch mehrere Partikel aussenden, die Eisstücke darstellen, aber dieses Thema wird in einem anderen Beitrag behandelt.

Abschluss

In diesem Tutorial haben wir einige Elemente implementiert, die erforderlich sind, um unseren Hockey-Prototyp in ein vollständig spielbares Spiel zu verwandeln. Ich habe den Fokus absichtlich auf die Konzepte hinter jedem dieser Elemente gelegt, anstatt sie tatsächlich in der Spiel-Engine X oder Y zu implementieren.

Der für das Spiel verwendete Freeze-and-Shatter-Ansatz mag zu fantastisch klingen, aber er hilft, das Projekt überschaubar zu halten. Sportregeln sind sehr spezifisch und ihre Umsetzung kann schwierig sein.

Indem Sie ein paar Bildschirme und einige HUD-Elemente hinzufügen, können Sie aus dieser Demo Ihr eigenes komplettes Hockeyspiel erstellen!

Verweise

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.