Bester Algorithmus für rekursive benachbarte Kacheln?

7

In meinem Spiel habe ich eine Reihe von Kacheln in einem 2D-Array platziert, die durch ihre Xs und Zs ([1,1], [1,2] usw.) gekennzeichnet sind.

Jetzt möchte ich eine Art "Farbeimer" -Mechanismus: Wenn Sie eine Kachel auswählen, werden alle benachbarten Kacheln zerstört, bis eine Bedingung sie stoppt, sagen wir, wenn sie ein Objekt mit trifft hasFlag.

Folgendes habe ich bisher, ich bin sicher, es ist ziemlich schlimm, es friert manchmal auch alles ein:

void destroyAdjacentTiles(int x, int z) {
    int GridSize = Cubes.GetLength(0);
    int minX = x == 0 ? x : x-1;
    int maxX = x == GridSize - 1 ? x : x+1;
    int minZ = z == 0 ? z : z-1;
    int maxZ = z == GridSize - 1 ? z : z+1;

    Debug.Log(string.Format("Cube: {0}, {1}; X {2}-{3}; Z {4}-{5}", x, z, minX, maxX, minZ, maxZ));

    for (int curX = minX; curX <= maxX; curX++) {
        for (int curZ = minZ; curZ <= maxZ; curZ++) {
            if (Cubes[curX, curZ] != Cubes[x, z]) {
                Debug.Log(string.Format("        Checking: {0}, {1}", curX, curZ));
                if (Cubes[curX,curZ] && Cubes[curX,curZ].GetComponent<CubeBehavior>().hasFlag) {
                    Destroy(Cubes[curX,curZ]);
                    destroyAdjacentTiles(curX, curZ);
                }
            }
        }
    }
}

Update: Ich versuche die rekursive Version des Flood-Fill-Algorithmus, habe aber Probleme. Um das westlich angrenzende Plättchen zu überprüfen, verwende ich x-1, für den Osten x + 1 usw. Das Problem ist, sobald x = 0, wird es eine Endlosschleife zwischen 0 und 1 geben. Ich muss das auch überprüfen Kachel existiert, also benutze ich ifs, um es herauszufinden, um keine zu bekommen IndexOutOfRangeException(ich habe versucht, try / catch zu verwenden, aber es ist so ziemlich das gleiche Ergebnis, ich dachte, das wäre einfacher). Hier ist mein Code (ja, ich reproduziere meinenSweeper):

bool destroyAdjacentTiles(int x, int z) {
    int GridSize = Cubes.GetLength(0);
    int minX = x == 0 ? x : x-1;
    int maxX = x == GridSize - 1 ? x : x+1;
    int minZ = z == 0 ? z : z-1;
    int maxZ = z == GridSize - 1 ? z : z+1;

    Debug.Log(string.Format("Cube: {0}, {1}; X {2}-{3}; Z {4}-{5}", x, z, minX, maxX, minZ, maxZ));

    CubeBehavior thisCube = Cubes[x, z].GetComponent<CubeBehavior>();

    if (thisCube.isMine) { Destroy(thisCube); Cubes[x,z] = null; return false; } // TO DO: Make this = game over
            // BELOW: Always, no matter what, causes the object to destroy, dunno why. So I removed it for the time being.
    //if (thisCube.surroundingMines >= 1) Destroy(thisCube); return true;

            // if the function doesn't return by now, means it's a to-be-destroyed cube, so destroy it
    //Destroy(thisCube.gameObject);
    //Cubes[x,z] = null;

            //
    if (x > 0) {
        if (Cubes[x-1,z].gameObject) destroyAdjacentTiles(x-1, z);
        if (x < GridSize - 1)
            if (Cubes[x+1,z].gameObject) destroyAdjacentTiles(x+1, z);
    }
    if (z > 0) {
        if (Cubes[x,z-1].gameObject) destroyAdjacentTiles(x, z-1);
        if (z < GridSize - 1)
            if (Cubes[x,z+1].gameObject) destroyAdjacentTiles(x, z+1);
    }

    return true;
}
casraf
quelle

Antworten:

12

Es macht keinen Sinn, das Rad neu zu erfinden. Es gibt einen vorhandenen Algorithmus, der das tut, was Sie wollen. Es heißt Flood Fill- Algorithmus. Die grundlegenden Schritte von Wikipedia:

Flood-fill (node, target-color, replacement-color):
 1. If the color of node is not equal to target-color, return. 
 2. Set the color of node to replacement-color.
 3. Perform Flood-fill (one step to the west of node, target-color, replacement-color).
     Perform Flood-fill (one step to the east of node, target-color, replacement-color).
     Perform Flood-fill (one step to the north of node, target-color, replacement-color).
     Perform Flood-fill (one step to the south of node, target-color, replacement-color).
  4. Return.

Anstatt die Farben zu ändern, werden Sie natürlich Kacheln zerstören, aber der Vorgang ist der gleiche. Sieht so aus, als würden Sie versuchen, eine 8-Wege-Flutung durchzuführen. Der Algorithmus ändert sich nur geringfügig, wenn Sie alle 8 orthogonalen und diagonalen Positionen anstelle der 4 orthogonalen wie im oben gezeigten Pseudocode überprüfen.

Beachten Sie auch, dass es beim Erstellen rekursiver Algorithmen sehr wichtig ist, eine klar definierte Grundbedingung zu haben. Der Grund, warum Sie das Programm von Zeit zu Zeit "einfrieren", ist, dass Sie in eine Endlosschleife geraten. Sie sollten überprüfen, ob die Kachel, auf der Sie spielen möchten, weder zerstört ist noch die Flagge hat.

Abhängig von der Größe Ihres Rasters (wenn es sehr, sehr groß wäre) können Sie einen nicht rekursiven Algorithmus verwenden. Ich glaube nicht, dass Sie auf ein Problem mit Stapelüberläufen stoßen würden, daher ist es nicht kritisch. Eine andere Methode, die komplexer, aber ziemlich interessant ist, ist die Festspeichermethode . Es wäre interessant zu implementieren, aber ich kann mir auch nicht vorstellen, dass Sie auf Speicherprobleme stoßen würden.

BEARBEITEN

Ich habe Ihren Code vereinfacht. Möglicherweise haben Sie die Dinge verwirrt, indem Sie versucht haben, die x- und z-Werte je nach Standort zu ändern. Definieren Sie einfach global das Minimum und Maximum für Ihr Raster und vergleichen Sie diese mit diesen. Sie müssen nur einmal prüfen, ob eine Position außerhalb der Grenzen liegt. Ihre Grundbedingungen sind:

  • Das Quadrat ist außerhalb der Grenzen (die erste Überprüfung von x und z im Vergleich zu ihren Grenzen)
  • Das Quadrat ist bereits explodiert (ich denke, Sie setzen die Kacheln auf null, um explodiert zu werden).
  • Square ist keine Mine (auf der Kachel ist das isMine-Flag nicht auf true gesetzt).

Wenn eine dieser Bedingungen erreicht ist, befinden Sie sich am Ende dieser Rekursion, sodass Sie die Rekursion auf diesem Pfad stoppen.

bool destroyAdjacentTiles(int x, int z) {

    if(x<minXBound || x > maxXBound || z < minZBound || z > maxZBound) {
        return false;
    }

    CubeBehavior thisCube = Cubes[x, z].GetComponent<CubeBehavior>();

    if(thisCube == null || !thisCube.isMine)
        return false;

    Destroy(thisCube);
    Cubes[x,z] = null;

    destroyAdjacentTiles(x-1, z);
    destroyAdjacentTiles(x+1, z);
    destroyAdjacentTiles(x, z-1);
    destroyAdjacentTiles(x, z+1);

    return true;
}
MichaelHouse
quelle
Hm, danke, das probiere ich mal aus. Scheint, dass logischerweise das Überprüfen von 4 Möglichkeiten schließlich in alle Richtungen überprüft? Oder bilde ich mir ein?
Casraf
Das ist richtig, aber es wird sich nicht durch einen diagonalen Durchgang quetschen. Sie können animierte Beispiele auf der Wikipedia-Site sehen.
MichaelHouse
Ich sehe, in meinem Fall brauche ich dann 4 Zeilen, ich kann es nicht über dünne Linien hinaus auslaufen lassen. Ich werde das ausprobieren, danke.
Casraf
Würden Sie bitte mein Update überprüfen?
Casraf
5

In solchen Fällen möchten Sie versuchen, rekursive Algorithmen zu vermeiden. Es ist besser, stapel- / warteschlangenbasierte Methoden zu verwenden, da diese schneller sind und keine Stapelüberläufe für große Bereiche verursachen. Diese Seite enthält Details und Quellenbeispiele für rekursive und stapelbasierte Flood-Fill-Algorithmen sowie Vergleiche der Ausführungsgeschwindigkeit: http://lodev.org/cgtutor/floodfill.html

Matthew R.
quelle
Das ist ein interessanter Link ... Ich bin nicht sehr vertraut mit Stacks, ich habe diese Klassen in der High School durchgeschlafen, haha. Wie schwer ist es, dies in C # zu reproduzieren?
Casraf
Es ist sehr einfach in C # zu implementieren. Verwenden Sie einfach die Stapelklasse : msdn.microsoft.com/en-us/library/system.collections.stack.aspx "Push" und "Pop" sind Methoden der Klasse für die Verwendung von "EmptyStack" "Klar"
Matthew R
Außerdem möchten Sie nur einen Hinweis von einem Stapel von Punkten verschieben / entfernen : msdn.microsoft.com/en-us/library/system.drawing.point.aspx Sie können also die X, Y-Komponenten verschieben / ziehen als Paare.
Matthew R