So verwenden Sie BSP-Bäume zur Erstellung von Game Maps

Wenn Sie einen Bereich zufällig mit Objekten füllen, wie z. B. Räume in einem zufälligen Dungeon, laufen Sie Gefahr, Dinge zu erstellen auch zufällig, was zu einem Klumpen oder nur zu einem unbrauchbaren Durcheinander führt. In diesem Tutorial zeige ich Ihnen, wie Sie es verwenden Binäre Raumaufteilung um dieses Problem zu lösen.

Ich werde Sie durch einige allgemeine Schritte führen, um mit BSP eine einfache 2D-Karte zu erstellen, die für ein Dungeon-Layout für ein Spiel verwendet werden kann. Ich werde dir zeigen, wie man ein Basic macht Blatt Objekt, das wir verwenden werden, um einen Bereich in kleine Segmente aufzuteilen; dann, wie man einen zufälligen Raum in jedem erzeugt Blatt; und schließlich, wie man alle Räume mit Fluren verbindet.

Hinweis: Während der Beispielcode hier AS3 verwendet, sollten Sie die Konzepte in nahezu jeder gewünschten Sprache verwenden können.

Demo-Projekt

Ich habe ein Demo-Programm erstellt, das die Leistungsfähigkeit von BSP demonstriert. Die Demo wurde mit Flixel geschrieben, einer kostenlosen Open-Source-AS3-Bibliothek zum Erstellen von Spielen.

Wenn Sie auf klicken Generieren Wenn Sie diese Taste drücken, wird derselbe Code wie oben ausgeführt, um einige zu generieren Blätter, und zieht sie dann an BitmapData Objekt, das dann angezeigt wird (vergrößert, um den Bildschirm auszufüllen).


Zufallskarte generieren. (Klicken Sie hier, um die Demo zu laden.)

Wenn Sie auf die abspielen Mit der Schaltfläche wird die erzeugte Karte übergeben Bitmap rüber zum FlxTilemap object, das dann eine abspielbare Tilemap generiert und auf dem Bildschirm anzeigt, auf der Sie sich bewegen können:


Die Karte spielen (Klicken Sie hier, um die Demo zu laden.)

Benutze die Pfeiltasten zur Bewegung.


Was ist BSP??

Binary Space Partitioning ist eine Methode zur Unterteilung eines Bereichs in kleinere Teile.

Grundsätzlich nehmen Sie ein Gebiet, genannt a Blatt, und teilen Sie es - entweder vertikal oder horizontal - in zwei kleinere Blätter, und wiederholen Sie dann den Vorgang an den kleineren Bereichen immer wieder, bis jeder Bereich mindestens so klein ist wie ein festgelegter Maximalwert.

Wenn Sie fertig sind, haben Sie eine Hierarchie der Partitionierung Blätter, mit denen du alle möglichen Dinge tun kannst. In 3D-Grafiken können Sie mit BSP sortieren, welche Objekte für den Player sichtbar sind, oder bei der Kollisionserkennung in kleineren, mundgerechten Teilen helfen.


Warum mit BSP Karten generieren??

Wenn Sie eine zufällige Karte erstellen möchten, gibt es verschiedene Möglichkeiten, dies zu tun. Sie können eine einfache Logik zum Erstellen von zufällig großen Rechtecken an zufälligen Positionen schreiben. Dies kann jedoch zu Landkarten führen, die voll von sich überlappenden, zusammengefügten oder seltsam beabstandeten Räumen sind. Es ist auch etwas schwieriger, die Räume miteinander zu verbinden und sicherzustellen, dass keine verwaisten Räume vorhanden sind.

Mit BSP können Sie gleichmäßigere Räume garantieren und gleichzeitig sicherstellen, dass Sie alle Räume miteinander verbinden können.


Blätter erstellen

Das erste, was wir brauchen, ist unser zu schaffen Blatt Klasse. Grundsätzlich unser Blatt wird ein Rechteck mit zusätzlichen Funktionen sein. Jeder Blatt wird entweder ein paar Kinder enthalten Blätter, oder ein Paar von Räume, sowie ein oder zwei Flure.

Hier ist was unser Blatt sieht aus wie:

öffentliche Klasse Leaf private const MIN_LEAF_SIZE: uint = 6; public var y: int, x: int, Breite: int, Höhe: int; // die Position und Größe dieses Blattes public var leftChild: Leaf; // das linke Kind des Blattes Leaf public var rightChild: Leaf; // das rechte Kind des Blattes Leaf public var room: Rectangle; // der Raum, der sich in dieser öffentlichen Halle von Leaf befindet: Vector .; // Flure, um dieses Blatt mit anderen öffentlichen Funktionen von Leafs zu verbinden Blatt (X: int, Y: int, Breite: int, Höhe: int) // initialisiert unser Blatt x = X; y = Y; Breite = Breite; Höhe = Höhe;  public function split (): Boolean // fängt an, das Blatt in zwei Kinder aufzuteilen, wenn (leftChild! = null || rightChild! = null) false zurückgibt; // wir sind schon gespalten! Abbrechen! // Richtung der Aufteilung bestimmen // Wenn die Breite> 25% größer als die Höhe ist, teilen wir uns vertikal auf // Wenn die Höhe> 25% größer ist als die Breite, teilen wir uns horizontal auf // Ansonsten teilen wir uns willkürlich auf: splitH: Boolean = FlxG.random ()> 0,5; if (width> height && width / height> = 1,25) splitH = false; sonst if (height> width && height / width> = 1,25) splitH = true; var max: int = (splitH? height: width) - MIN_LEAF_SIZE; // Bestimmen Sie die maximale Höhe oder Breite wenn (max <= MIN_LEAF_SIZE) return false; // the area is too small to split any more… var split:int = Registry.randomNumber(MIN_LEAF_SIZE, max); // determine where we're going to split // create our left and right children based on the direction of the split if (splitH)  leftChild = new Leaf(x, y, width, split); rightChild = new Leaf(x, y + split, width, height - split);  else  leftChild = new Leaf(x, y, split, height); rightChild = new Leaf(x + split, y, width - split, height);  return true; // split successful!  

Nun müssen Sie tatsächlich Ihre erstellen Blätter:

const MAX_LEAF_SIZE: uint = 20; var _leafs: Vektor = neuer Vektor; Var l: Blatt; // helfer Blatt // erstelle ein Blatt als 'Wurzel' aller Blätter. var root: Blatt = neues Blatt (0, 0, _sprMap.width, _sprMap.height); _leafs.push (Wurzel); var did_split: Boolean = true; // wir durchlaufen jedes Blatt in unserem Vector immer wieder, bis keine Blätter mehr geteilt werden können. while (did_split) did_split = falsch; für jedes (l in _leafs) if (l.leftChild == null && l.rightChild == null) // wenn dieses Blatt nicht bereits geteilt ist… // wenn dieses Blatt zu groß ist, oder eine Chance von 75%… wenn (l.width> MAX_LEAF_SIZE || l.height> MAX_LEAF_SIZE || FlxG.random ()> 0,25) if (l.split ()) // das Blatt teilen! // Wenn wir geteilt haben, schieben Sie die untergeordneten Blätter in den Vektor, damit wir als nächstes in die Schleife springen können. _leafs.push (l.leftChild); _leafs.push (l.rightChild); did_split = true; 

Nachdem diese Schleife beendet ist, werden Sie mit einem Vektor (ein typisiertes Array) voll von all Ihren Blätter.

Hier ist ein Beispiel, bei dem die Zeilen voneinander getrennt sind Blatt:


Beispiel für ein Gebiet, geteilt durch Blätter

Räume schaffen

Nun das dein Blätter definiert sind, müssen wir die Räume machen. Wir wollen eine Art "Rieseleffekt", bei dem wir von unserer größten "Wurzel" gehen Blatt bis zum kleinsten Blätter ohne Kinder, und dann machen Sie in jedem von ihnen ein Zimmer.

Fügen Sie diese Funktion also Ihrem hinzu Blatt Klasse:

public function createRooms (): void // Diese Funktion generiert alle Räume und Flure für dieses Blatt und alle seine Kinder. if (leftChild! = null || rightChild! = null) // Dieses Blatt wurde geteilt. Gehen Sie also in die untergeordneten Blätter, wenn (leftChild! = null) leftChild.createRooms ();  if (rightChild! = null) rightChild.createRooms ();  else // Dieses Blatt ist bereit, einen Raum zu erstellen. var roomSize: Point; var roomPos: Punkt; // der Raum kann zwischen 3 x 3 Kacheln bis zur Größe des Blattes sein - 2. roomSize = new Point (Registry.randomNumber (3, width - 2), Registry.randomNumber (3, height - 2)); // Platziere den Raum innerhalb des Blattes, aber lege ihn nicht richtig // gegen die Seite des Blattes (wodurch Räume zusammengefügt werden) roomPos = new Point (Registry.randomNumber (1, Breite - roomSize.x - 1)) , Registry.randomNumber (1, height - roomSize.y - 1)); room = new Rectangle (x + roomPos.x, y + roomPos.y, roomSize.x, roomSize.y); 

Dann, nachdem Sie Ihre erstellt haben Vektor von Blätter, Rufen Sie unsere neue Funktion von Ihrem Stamm auf Blatt:

_leafs = neuer Vektor; Var l: Blatt; // helfer Blatt // erstelle ein Blatt als 'Wurzel' aller Blätter. var root: Blatt = neues Blatt (0, 0, _sprMap.width, _sprMap.height); _leafs.push (Wurzel); var did_split: Boolean = true; // wir durchlaufen jedes Blatt in unserem Vector immer wieder, bis keine Blätter mehr geteilt werden können. while (did_split) did_split = falsch; für jedes (l in _leafs) if (l.leftChild == null && l.rightChild == null) // wenn dieses Blatt nicht bereits geteilt ist… // wenn dieses Blatt zu groß ist, oder eine Chance von 75% ... (l.width> MAX_LEAF_SIZE || l.height> MAX_LEAF_SIZE || FlxG.random ()> 0,25) if (l.split ()) // das Blatt teilen! // Wenn wir uns geteilt haben, schieben Sie die untergeordneten Blätter in den Vektor, damit wir als nächstes in sie hineinlaufen können. _leafs.push (l.leftChild); _leafs.push (l.rightChild); did_split = true;  // Als nächstes durchlaufen Sie jedes Blatt und erstellen einen Raum in jedem. root.createRooms ();

Hier ist ein Beispiel für einige generierte Blätter mit Räumen in ihnen:


Auswahl von Blättern mit zufälligem Raum in jedem.

Wie Sie sehen können, jeder Blatt enthält einen Raum mit zufälliger Größe und Position. Sie können mit den Werten für Minimum und Maximum spielen Blatt Größe und ändern Sie, wie Sie die Größe und Position jedes Raums bestimmen, um unterschiedliche Effekte zu erzielen.

Wenn wir unsere entfernen Blatt Trennlinien können Sie sehen, dass die Räume die gesamte Karte gut ausfüllen - es gibt nicht viel Platzverschwendung - und sie wirken ein bisschen organischer.


Eine Probe von Blätter mit einem Raum in jedem Raum, wobei Trennlinien entfernt wurden.

Blätter verbinden

Jetzt müssen wir nur noch jeden Raum miteinander verbinden. Zum Glück haben wir die eingebauten Beziehungen zwischen Blätter, Wir müssen nur sicherstellen, dass jeder Blatt das hat Kind Blätter hat einen Flur, der seine Kinder verbindet.

Wir nehmen eine Blatt, Schauen Sie sich jedes seiner Kinder an Blätter, gehen Sie den ganzen Weg durch jedes Kind bis wir zu einem kommen Blatt mit einem Raum und verbinden Sie dann die Räume miteinander. Wir können dies gleichzeitig tun, wenn wir unsere Räume generieren.

Erstens brauchen wir eine neue Funktion, um von jeder zu iterieren Blatt in einen der Räume, die sich in einem der Kinder befinden Blätter:

public function getRoom (): Rectangle // Durchlaufe alle Blätter, um einen Raum zu finden, falls vorhanden. if (room! = null) Rückführungsraum; else var lRoom: Rechteck; var rRoom: Rechteck; if (leftChild! = null) lRoom = leftChild.getRoom ();  if (rightChild! = null) rRoom = rightChild.getRoom ();  if (lRoom == null && rRoom == null) gibt null zurück; else if (rRoom == null) gibt lRoom zurück; sonst if (lRoom == null) return rRoom; else if (FlxG.random ()> .5) return lRoom; sonst Rückkehr rRoom; 

Als Nächstes benötigen wir eine Funktion, die ein Raumpaar benötigt, einen zufälligen Punkt in beiden Räumen auswählt und dann entweder ein oder zwei Rechtecke mit zwei Kacheln erstellt, um die Punkte miteinander zu verbinden.

public function createHall (l: Rechteck, r: Rechteck): void // jetzt verbinden wir diese beiden Räume mit Fluren. // Das sieht ziemlich kompliziert aus, aber es versucht nur herauszufinden, wo sich der Punkt befindet, und dann entweder eine gerade Linie oder ein Linienpaar zu zeichnen, um einen rechten Winkel zu bilden, um sie zu verbinden. // Sie könnten eine zusätzliche Logik verwenden, um Ihre Hallen biegsamer zu gestalten, oder einige fortgeschrittenere Dinge tun, wenn Sie dies wünschen. Hallen = neuer Vektor; var point1: Punkt = neuer Punkt (Registry.randomNumber (l.lft + 1, l.right - 2), Registry.randomNumber (l.top + 1, l.bottom - 2)); var point2: Punkt = neuer Punkt (Registry.randomNumber (r.left + 1, r.right - 2), Registry.randomNumber (r.top + 1, r.bottom - 2)); var w: Number = Punkt 2.x - Punkt 1.x; var h: Zahl = Punkt 2.y - Punkt 1.y; wenn (w < 0)  if (h < 0)  if (FlxG.random() < 0.5)  halls.push(new Rectangle(point2.x, point1.y, Math.abs(w), 1)); halls.push(new Rectangle(point2.x, point2.y, 1, Math.abs(h)));  else  halls.push(new Rectangle(point2.x, point2.y, Math.abs(w), 1)); halls.push(new Rectangle(point1.x, point2.y, 1, Math.abs(h)));   else if (h > 0) if (FlxG.random () < 0.5)  halls.push(new Rectangle(point2.x, point1.y, Math.abs(w), 1)); halls.push(new Rectangle(point2.x, point1.y, 1, Math.abs(h)));  else  halls.push(new Rectangle(point2.x, point2.y, Math.abs(w), 1)); halls.push(new Rectangle(point1.x, point1.y, 1, Math.abs(h)));   else // if (h == 0)  halls.push(new Rectangle(point2.x, point2.y, Math.abs(w), 1));   else if (w > 0) if (h < 0)  if (FlxG.random() < 0.5)  halls.push(new Rectangle(point1.x, point2.y, Math.abs(w), 1)); halls.push(new Rectangle(point1.x, point2.y, 1, Math.abs(h)));  else  halls.push(new Rectangle(point1.x, point1.y, Math.abs(w), 1)); halls.push(new Rectangle(point2.x, point2.y, 1, Math.abs(h)));   else if (h > 0) if (FlxG.random () < 0.5)  halls.push(new Rectangle(point1.x, point1.y, Math.abs(w), 1)); halls.push(new Rectangle(point2.x, point1.y, 1, Math.abs(h)));  else  halls.push(new Rectangle(point1.x, point2.y, Math.abs(w), 1)); halls.push(new Rectangle(point1.x, point1.y, 1, Math.abs(h)));   else // if (h == 0)  halls.push(new Rectangle(point1.x, point1.y, Math.abs(w), 1));   else // if (w == 0)  if (h < 0)  halls.push(new Rectangle(point2.x, point2.y, 1, Math.abs(h)));  else if (h > 0) halls.push (neues Rechteck (Punkt 1.x, Punkt 1.j, 1, Math.abs (h))); 

Schließlich ändern Sie Ihre createRooms () Funktion zum Aufrufen der createHall () Funktion auf jedem Blatt das hat ein paar kinder:

public function createRooms (): void // Diese Funktion generiert alle Räume und Flure für dieses Blatt und alle seine Kinder. if (leftChild! = null || rightChild! = null) // Dieses Blatt wurde geteilt. Gehen Sie also in die untergeordneten Blätter, wenn (leftChild! = null) leftChild.createRooms ();  if (rightChild! = null) rightChild.createRooms ();  // Wenn sich in diesem Blatt sowohl linke als auch rechte Kinder befinden, erstellen Sie einen Flur zwischen if (leftChild! = null && rightChild! = null) createHall (leftChild.getRoom (), rightChild.getRoom ());  else // Dieses Blatt ist bereit, einen Raum zu erstellen. var roomSize: Point; var roomPos: Punkt; // der Raum kann zwischen 3 x 3 Kacheln bis zur Größe des Blattes sein - 2. roomSize = new Point (Registry.randomNumber (3, width - 2), Registry.randomNumber (3, height - 2)); // Platziere den Raum innerhalb des Blattes, aber lege ihn nicht direkt an die Seite des Blattes (dies würde Räume zusammenfügen) roomPos = new Point (Registry.randomNumber (1, Breite - roomSize.x - 1), Registry .andomNumber (1, height - roomSize.y - 1)); room = new Rectangle (x + roomPos.x, y + roomPos.y, roomSize.x, roomSize.y); 

Ihre Räume und Flure sollten jetzt etwa so aussehen:


Beispiel von Blätter gefüllt mit zufälligen Räumen, die über Flure miteinander verbunden sind.

Wie Sie sehen können, stellen wir sicher, dass wir alle miteinander verbinden Blatt, Wir haben keine verwaisten Zimmer mehr. Offensichtlich könnte die Flurlogik etwas verfeinert werden, um zu vermeiden, zu nahe an anderen Fluren zu laufen, aber sie funktioniert gut genug.


Beenden

Das ist es im Grunde! Wir haben beschrieben, wie Sie ein (relativ) einfaches erstellen Blatt Objekt, mit dem Sie einen Baum aus unterteilten Blättern erzeugen können, erzeugen Sie einen zufälligen Raum in jedem Blatt, und verbinden Sie die Räume über Flure.

Derzeit sind alle Objekte, die wir erstellt haben, im Wesentlichen Rechtecke. Abhängig davon, wie Sie den resultierenden Dungeon verwenden möchten, können Sie sie auf verschiedene Arten handhaben.

Jetzt können Sie BSP verwenden, um beliebige Arten von zufälligen Karten zu erstellen, oder es verwenden, um Power-Ups oder Feinde gleichmäßig über ein Gebiet zu verteilen ... oder was immer Sie möchten!