Lenkverhalten verstehen: Bewegungsmanager
German (Deutsch) translation by Alex Grigorovich (you can also view the original English article)
Das Lenkverhalten eignet sich hervorragend, um realistische Bewegungsmuster zu erstellen, aber sie sind noch besser, wenn Sie sie leicht steuern, verwenden und kombinieren können. In diesem Tutorial werde ich die Implementierung eines Bewegungsmanagers für alle unsere zuvor besprochenen Verhaltensweisen diskutieren und behandeln.
Hinweis: Obwohl dieses Tutorial mit AS3 und Flash geschrieben wurde, sollten Sie in der Lage sein, dieselben Techniken und Konzepte in fast jeder Spieleentwicklungsumgebung zu verwenden. Sie müssen über ein grundlegendes Verständnis von mathematischen Vektoren verfügen.
Lenkungskräfte kombinieren
Wie zuvor erörtert, erzeugt jedes Lenkverhalten eine resultierende Kraft (eine sogenannte "Lenkkraft"), die zum Geschwindigkeitsvektor addiert wird. Die Richtung und Größe dieser Kraft wird den Charakter antreiben und ihn nach einem Muster bewegen (suchen, fliehen, wandern usw.). Die allgemeine Berechnung lautet:
1 |
steering = seek(); // this can be any behavior |
2 |
steering = truncate (steering, max_force) |
3 |
steering = steering / mass |
4 |
|
5 |
velocity = truncate (velocity + steering , max_speed) |
6 |
position = position + velocity |
Da die Lenkkraft ein Vektor ist, kann sie zu jedem anderen Vektor addiert werden (genauso wie die Geschwindigkeit). Die wahre "Magie" liegt jedoch in der Tatsache, dass Sie mehrere Lenkkräfte addieren können - es ist so einfach wie:
1 |
steering = nothing(); // the null vector, meaning "zero force magnitude" |
2 |
steering = steering + seek(); |
3 |
steering = steering + flee(); |
4 |
(...) |
5 |
steering = truncate (steering, max_force) |
6 |
steering = steering / mass |
7 |
|
8 |
velocity = truncate (velocity + steering , max_speed) |
9 |
position = position + velocity |
Die kombinierten Lenkkräfte ergeben einen Vektor, der all diese Kräfte repräsentiert. Im obigen Code-Schnipsel wird die resultierende Lenkkraft den Charakter dazu bringen, etwas zu suchen, während er gleichzeitig nach etwas anderem flieht.
Sehen Sie sich nachfolgend einige Beispiele von Lenkkräften an, die zu einer einzigen Lenkkraft kombiniert werden:


Lenkkräfte vereint.Komplexe Muster mühelos
Durch die Kombination der Lenkkräfte werden mühelos extrem komplexe Bewegungsmuster erzeugt. Stellen Sie sich vor, wie schwer es wäre, Code zu schreiben, um einen Charakter dazu zu bringen, etwas zu suchen, aber gleichzeitig einen bestimmten Bereich zu vermeiden, ohne Vektoren und Kräfte zu verwenden?
Das würde die Berechnung von Entfernungen, Flächen, Wegen, Grafiken und dergleichen erfordern. Wenn sich die Dinge bewegen, müssen all diese Berechnungen von Zeit zu Zeit wiederholt werden, da sich die Umgebung ständig ändert.
Beim Lenkverhalten sind alle Kräfte dynamisch. Sie sollen bei jedem Spielupdate berechnet werden, damit sie natürlich und nahtlos auf Umgebungsänderungen reagieren.
Die folgende Demo zeigt Schiffe, die den Mauszeiger suchen, aber gleichzeitig aus der Mitte des Bildschirms fliehen:
Bewegungsmanager
Um auf einfache und einfache Weise mehrere Lenkverhalten gleichzeitig nutzen zu können, bietet sich ein Bewegungsmanager an. Die Idee ist, eine "Black Box" zu erstellen, die an jede vorhandene Entität angeschlossen werden kann, um diese Verhaltensweisen ausführen zu können.
Der Manager hat einen Verweis auf die Entität, an die er angeschlossen ist (der "Host"). Der Manager stellt dem Host eine Reihe von Methoden zur Verfügung, z. B. seek() und flee(). Jedes Mal, wenn solche Verfahren aufgerufen werden, aktualisiert der Manager seine internen Eigenschaften, um einen Lenkkraftvektor zu erzeugen.
Nachdem der Manager alle Aufrufe verarbeitet hat, fügt er die resultierende Lenkkraft zum Geschwindigkeitsvektor des Hosts hinzu. Dadurch werden Größe und Richtung des Geschwindigkeitsvektors des Hosts entsprechend dem aktiven Verhalten geändert.
Die folgende Abbildung zeigt die Architektur:
Bewegungsmanager: Plugin-Architektur. Dinge generisch machen
Der Manager verfügt über eine Reihe von Methoden, von denen jede ein bestimmtes Verhalten repräsentiert. Jedes Verhalten muss mit unterschiedlichen externen Informationen versorgt werden, um zu funktionieren.
Das Suchverhalten benötigt beispielsweise einen Punkt im Raum, der verwendet wird, um die Lenkkraft zu diesem Ort zu berechnen; verfolgen benötigt mehrere Informationen von seinem Ziel, wie z. B. die aktuelle Position und Geschwindigkeit. Ein Punkt im Raum kann als Instanz von Point oder Vector2D ausgedrückt werden, beides ziemlich gängige Klassen in jedem Framework.
Das im Verfolgungsverhalten verwendete Ziel kann jedoch alles sein. Um den Bewegungsmanager generisch genug zu gestalten, muss er ein Ziel erhalten, das unabhängig von seinem Typ in der Lage ist, einige "Fragen" zu beantworten, wie beispielsweise "Wie hoch ist Ihre aktuelle Geschwindigkeit?". Mit einigen Prinzipien der objektorientierten Programmierung kann dies mit Schnittstellen erreicht werden.
Angenommen, die Schnittstelle IBoid beschreibt eine Entität, die vom Bewegungsmanager verarbeitet werden kann, jede Klasse im Spiel kann Steuerverhalten verwenden, solange sie IBoid implementiert. Diese Schnittstelle hat die folgende Struktur:
1 |
public interface IBoid |
2 |
{
|
3 |
function getVelocity() :Vector3D; |
4 |
function getMaxVelocity() :Number; |
5 |
function getPosition() :Vector3D; |
6 |
function getMass() :Number; |
7 |
} |
Struktur des Bewegungsmanagers
Da der Manager nun generisch mit allen Spielelementen interagieren kann, kann seine Grundstruktur erstellt werden. Der Manager besteht aus zwei Eigenschaften (der resultierenden Steuerungskraft und der Hostreferenz) und einer Reihe öffentlicher Methoden, eine für jedes Verhalten:
1 |
public class SteeringManager |
2 |
{
|
3 |
public var steering :Vector3D; |
4 |
public var host :IBoid; |
5 |
|
6 |
// The constructor |
7 |
public function SteeringManager(host :IBoid) {
|
8 |
this.host = host; |
9 |
this.steering = new Vector3D(0, 0); |
10 |
} |
11 |
|
12 |
// The public API (one method for each behavior) |
13 |
public function seek(target :Vector3D, slowingRadius :Number = 20) :void {}
|
14 |
public function flee(target :Vector3D) :void {}
|
15 |
public function wander() :void {}
|
16 |
public function evade(target :IBoid) :void {}
|
17 |
public function pursuit(target :IBoid) :void {}
|
18 |
|
19 |
// The update method. |
20 |
// Should be called after all behaviors have been invoked |
21 |
public function update() :void {}
|
22 |
|
23 |
// Reset the internal steering force. |
24 |
public function reset() :void {}
|
25 |
|
26 |
// The internal API |
27 |
private function doSeek(target :Vector3D, slowingRadius :Number = 0) :Vector3D {}
|
28 |
private function doFlee(target :Vector3D) :Vector3D {}
|
29 |
private function doWander() :Vector3D {}
|
30 |
private function doEvade(target :IBoid) :Vector3D {}
|
31 |
private function doPursuit(target :IBoid) :Vector3D {}
|
32 |
} |
Wenn der Manager instanziiert wird, muss er eine Referenz auf den Host erhalten, an den er angeschlossen ist. Dadurch kann der Manager den Host-Geschwindigkeitsvektor entsprechend dem aktiven Verhalten ändern.
Jedes Verhalten wird durch zwei Methoden repräsentiert, eine öffentliche und eine private. Verwenden Sie die Suche als Beispiel:
1 |
public function seek(target :Vector3D, slowingRadius :Number = 20) :void {}
|
2 |
private function doSeek(target :Vector3D, slowingRadius :Number = 0) :Vector3D {}
|
Das öffentliche seek() wird aufgerufen, um den Manager anzuweisen, dieses spezifische Verhalten anzuwenden. Die Methode hat keinen Rückgabewert und ihre Parameter beziehen sich auf das Verhalten selbst, beispielsweise einen Punkt im Raum. Unter der Haube wird die private Methode doSeek() aufgerufen und ihr Rückgabewert, die berechnete Lenkkraft für dieses spezifische Verhalten, wird zur lenk-Eigenschaft des Managers hinzugefügt.
Der folgende Code demonstriert die Implementierung von seek:
1 |
// The publish method. |
2 |
// Receives a target to seek and a slowingRadius (used to perform arrive). |
3 |
public function seek(target :Vector3D, slowingRadius :Number = 20) :void {
|
4 |
steering.incrementBy(doSeek(target, slowingRadius)); |
5 |
} |
6 |
|
7 |
// The real implementation of seek (with arrival code included) |
8 |
private function doSeek(target :Vector3D, slowingRadius :Number = 0) :Vector3D {
|
9 |
var force :Vector3D; |
10 |
var distance :Number; |
11 |
|
12 |
desired = target.subtract(host.getPosition()); |
13 |
|
14 |
distance = desired.length; |
15 |
desired.normalize(); |
16 |
|
17 |
if (distance <= slowingRadius) {
|
18 |
desired.scaleBy(host.getMaxVelocity() * distance/slowingRadius); |
19 |
} else {
|
20 |
desired.scaleBy(host.getMaxVelocity()); |
21 |
} |
22 |
|
23 |
force = desired.subtract(host.getVelocity()); |
24 |
|
25 |
return force; |
26 |
} |
Alle anderen Verhaltensmethoden werden auf sehr ähnliche Weise implementiert. Die pursuit()-Methode sieht beispielsweise folgendermaßen aus:
1 |
public function pursuit(target :IBoid) :void {
|
2 |
steering.incrementBy(doPursuit(target)); |
3 |
} |
4 |
|
5 |
private function doPursuit(target :IBoid) :Vector3D {
|
6 |
distance = target.getPosition().subtract(host.getPosition()); |
7 |
|
8 |
var updatesNeeded :Number = distance.length / host.getMaxVelocity(); |
9 |
|
10 |
var tv :Vector3D = target.getVelocity().clone(); |
11 |
tv.scaleBy(updatesNeeded); |
12 |
|
13 |
targetFuturePosition = target.getPosition().clone().add(tv); |
14 |
|
15 |
return doSeek(targetFuturePosition); |
16 |
} |
Mit dem Code aus vorherigen Tutorials müssen Sie diese lediglich in Form von behavior() und doBehavior() anpassen, damit sie dem Bewegungsmanager hinzugefügt werden können.
Anwenden und Aktualisieren von Lenkkräften
Jedes Mal, wenn die Methode eines Verhaltens aufgerufen wird, wird die resultierende Kraft, die es erzeugt, der steering-Eigenschaft des Managers hinzugefügt. Infolgedessen sammelt diese Eigenschaft alle Lenkkräfte an.
Wenn alle Verhaltensweisen aufgerufen wurden, muss der Manager die aktuelle Lenkkraft auf die Host-Geschwindigkeit anwenden, damit er sich gemäß den aktiven Verhaltensweisen bewegt. Es wird in der update()-Methode des Bewegungsmanagers ausgeführt:
1 |
public function update():void {
|
2 |
var velocity :Vector3D = host.getVelocity(); |
3 |
var position :Vector3D = host.getPosition(); |
4 |
|
5 |
truncate(steering, MAX_FORCE); |
6 |
steering.scaleBy(1 / host.getMass()); |
7 |
|
8 |
velocity.incrementBy(steering); |
9 |
truncate(velocity, host.getMaxVelocity()); |
10 |
|
11 |
position.incrementBy(velocity); |
12 |
} |
Die obige Methode muss vom Host (oder einer anderen Spieleinheit) aufgerufen werden, nachdem alle Verhaltensweisen aufgerufen wurden, andernfalls wird der Host seinen Geschwindigkeitsvektor niemals ändern, um den aktiven Verhaltensweisen zu entsprechen.
Verwendung
Nehmen wir an, eine Klasse namens Prey soll sich mit dem Steuerverhalten bewegen, hat aber im Moment weder einen Steuercode noch den Bewegungsmanager. Seine Struktur wird wie folgt aussehen:
1 |
public class Prey |
2 |
{
|
3 |
public var position :Vector3D; |
4 |
public var velocity :Vector3D; |
5 |
public var mass :Number; |
6 |
|
7 |
public function Prey(posX :Number, posY :Number, totalMass :Number) {
|
8 |
position = new Vector3D(posX, posY); |
9 |
velocity = new Vector3D(-1, -2); |
10 |
mass = totalMass; |
11 |
|
12 |
x = position.x; |
13 |
y = position.y; |
14 |
} |
15 |
|
16 |
public function update():void {
|
17 |
velocity.normalize(); |
18 |
velocity.scaleBy(MAX_VELOCITY); |
19 |
velocity.scaleBy(1 / mass); |
20 |
|
21 |
truncate(velocity, MAX_VELOCITY); |
22 |
position = position.add(velocity); |
23 |
|
24 |
x = position.x; |
25 |
y = position.y; |
26 |
} |
27 |
} |
Mit dieser Struktur können die Klasseninstanzen mithilfe der Euler-Integration verschoben werden, genau wie in der ersten Demo des Such-Tutorials. Um den Manager verwenden zu können, benötigt es eine Eigenschaft, die auf den Bewegungsmanager verweist, und es muss die IBoid-Schnittstelle implementieren:
1 |
public class Prey implements IBoid |
2 |
{
|
3 |
public var position :Vector3D; |
4 |
public var velocity :Vector3D; |
5 |
public var mass :Number; |
6 |
public var steering :SteeringManager; |
7 |
|
8 |
public function Prey(posX :Number, posY :Number, totalMass :Number) {
|
9 |
position = new Vector3D(posX, posY); |
10 |
velocity = new Vector3D(-1, -2); |
11 |
mass = totalMass; |
12 |
steering = new SteeringManager(this); |
13 |
|
14 |
x = position.x; |
15 |
y = position.y; |
16 |
} |
17 |
|
18 |
public function update():void {
|
19 |
velocity.normalize(); |
20 |
velocity.scaleBy(MAX_VELOCITY); |
21 |
velocity.scaleBy(1 / mass); |
22 |
|
23 |
truncate(velocity, MAX_VELOCITY); |
24 |
position = position.add(velocity); |
25 |
|
26 |
x = position.x; |
27 |
y = position.y; |
28 |
} |
29 |
|
30 |
// Below are the methods the interface IBoid requires. |
31 |
|
32 |
public function getVelocity() :Vector3D {
|
33 |
return velocity; |
34 |
} |
35 |
|
36 |
public function getMaxVelocity() :Number {
|
37 |
return 3; |
38 |
} |
39 |
|
40 |
public function getPosition() :Vector3D {
|
41 |
return position; |
42 |
} |
43 |
|
44 |
public function getMass() :Number {
|
45 |
return mass; |
46 |
} |
47 |
} |
Die update()-Methode muss entsprechend geändert werden, damit auch der Manager aktualisiert werden kann:
1 |
public function update():void {
|
2 |
// Make the prey wander around... |
3 |
steering.wander(); |
4 |
|
5 |
// Update the manager so it will change the prey velocity vector. |
6 |
// The manager will perform the Euler intergration as well, changing |
7 |
// the "position" vector. |
8 |
steering.update(); |
9 |
|
10 |
// After the manager has updated its internal structures, all we must |
11 |
// do is update our position according to the "position" vector. |
12 |
x = position.x; |
13 |
y = position.y; |
14 |
} |
Alle Verhaltensweisen können gleichzeitig verwendet werden, solange alle Methodenaufrufe vor dem update()-Aufruf des Managers erfolgen, der die akkumulierte Lenkkraft auf den Geschwindigkeitsvektor des Hosts anwendet.
Der folgende Code demonstriert eine andere Version der update()-Methode von Prey, aber dieses Mal sucht sie eine Position auf der Karte und vermeidet ein anderes Zeichen (beides gleichzeitig):
1 |
public function update():void {
|
2 |
var destination :Vector3D = getDestination(); // the place to seek |
3 |
var hunter :IBoid = getHunter(); // get the entity who is hunting us |
4 |
|
5 |
// Seek the destination and evade the hunter (at the same time!) |
6 |
steering.seek(destination); |
7 |
steering.evade(hunter); |
8 |
|
9 |
// Update the manager so it will change the prey velocity vector. |
10 |
// The manager will perform the Euler intergration as well, changing |
11 |
// the "position" vector. |
12 |
steering.update(); |
13 |
|
14 |
// After the manager has updated its internal structures, all we must |
15 |
// do is update our position according to the "position" vector. |
16 |
x = position.x; |
17 |
y = position.y; |
18 |
} |
Demo
Die folgende Demo zeigt ein komplexes Bewegungsmuster, bei dem mehrere Verhaltensweisen kombiniert werden. Es gibt zwei Arten von Charakteren in der Szene: den Jäger und die Beute.
Der Jäger verfolgt eine Beute, wenn sie nahe genug kommt; es wird so lange andauern, wie die Ausdauerversorgung reicht; Wenn ihm die Ausdauer ausgeht, wird die Verfolgung unterbrochen und der Jäger wandert, bis er seine Ausdauer wiedererlangt.
Hier ist die update()-Methode von Hunter:
1 |
public function update():void {
|
2 |
if (resting && stamina++ >= MAX_STAMINA) {
|
3 |
resting = false; |
4 |
} |
5 |
|
6 |
if (prey != null && !resting) {
|
7 |
steering.pursuit(prey); |
8 |
stamina -= 2; |
9 |
|
10 |
if (stamina <= 0) {
|
11 |
prey = null; |
12 |
resting = true; |
13 |
} |
14 |
} else {
|
15 |
steering.wander(); |
16 |
prey = getClosestPrey(position); |
17 |
} |
18 |
|
19 |
steering.update(); |
20 |
|
21 |
x = position.x; |
22 |
y = position.y; |
23 |
} |
Die Beute wandert auf unbestimmte Zeit. Kommt der Jäger zu nahe, weicht er aus. Wenn sich der Mauszeiger in der Nähe befindet und kein Jäger in der Nähe ist, sucht die Beute den Mauszeiger.
Hier ist die update()-Methode von Prey:
1 |
public function update():void {
|
2 |
var distance :Number = Vector3D.distance(position, Game.mouse); |
3 |
|
4 |
hunter = getHunterWithinRange(position); |
5 |
|
6 |
if (hunter != null) {
|
7 |
steering.evade(hunter); |
8 |
} |
9 |
|
10 |
if (distance <= 300 && hunter == null) {
|
11 |
steering.seek(Game.mouse, 30); |
12 |
|
13 |
} else if(hunter == null){
|
14 |
steering.wander(); |
15 |
} |
16 |
|
17 |
steering.update(); |
18 |
|
19 |
x = position.x; |
20 |
y = position.y; |
21 |
} |
Das Endergebnis (grau ist wandern, grün ist suchen, orange ist verfolgen, rot ist ausweichen):
Abschluss
Ein Bewegungsmanager ist sehr nützlich, um mehrere Lenkverhalten gleichzeitig zu steuern. Die Kombination solcher Verhaltensweisen kann sehr komplexe Bewegungsmuster erzeugen, die es einer Spieleinheit ermöglichen, eine Sache zu suchen und gleichzeitig einer anderen auszuweichen.
Ich hoffe, Ihnen hat das in diesem Tutorial besprochene und implementierte Managementsystem gefallen und Sie können es in Ihren Spielen verwenden. Danke fürs Lesen! Vergessen Sie nicht, auf dem Laufenden zu bleiben, indem Sie uns auf Twitter, Facebook oder Google+ folgen.



