Startseite

Match-3-Spiel — Erste Schritte

Willkommen zu diesem Unity-2D-Tutorial. In dieser Lektion erstellen wir Schritt für Schritt eine Basis für ein Match-3-Spiel. Vorherige Unity-Erfahrung ist nicht erforderlich. Installiere Unity 6.0 LTS (oder eine andere Version mit 2D-Unterstützung), falls noch nicht geschehen — und los geht’s.

Es gibt viele Möglichkeiten, ein solches Spiel zu entwickeln. Wir erstellen eine einfache, aber flexible Umsetzung mit einem 2D-Raum, Sprites und deren Bewegung via transform.Translate. Zum Erkennen ausgewählter Elemente nutzen wir das Raycast 2D-System. Die Spiellogik teilen wir in separate Module auf, damit der Code klar und leicht erweiterbar bleibt.

Schritt 1. Projekt erstellen

Öffne den Unity Hub und erstelle ein neues 2D-Projekt, indem du die Vorlage 2D (Built-In render Pipeline) auswählst. Gehe dazu zum Tab Projects und klicke auf New Project. Suche die Vorlage 2D (Built-In render Pipeline) — sie enthält bereits grundlegende 2D-Grafik und Physik. Je nach deiner Unity-Version kann der Name leicht abweichen — wenn du sie nicht findest, wähle 2D (URP) oder einfach 2D.

Gib einen Projektnamen ein, z. B. MatchGame (oder einen eigenen), und wähle den Ordner, in dem es erstellt werden soll. Klicke auf Create project und warte, bis die Umgebung vorbereitet ist.

Schritt 2. Sprite importieren

Für diesen Prototyp reicht uns ein einzelnes Sprite, dessen Aussehen wir später durch Farbänderungen variieren.

Du kannst das unten stehende Bild verwenden oder dein eigenes wählen. Die einzige Anforderung: Das Sprite sollte 100×100 Pixel groß sein, da dies die Grundgröße der Elemente auf dem Spielfeld ist.

Diamant-Sprite
Klicke auf das Bild, um es herunterzuladen.

Um das Bild in Unity zu importieren: Klicke mit der rechten Maustaste im Fenster Assets, wähle Import New Asset..., suche die heruntergeladene Datei und klicke auf Import.

💡 Tipp: Du kannst die Bilddatei auch direkt ins Assets-Fenster ziehen — das geht oft noch schneller.
💡 Tipp: Fang einfach an — die Optik kannst du später jederzeit verbessern.

Schritt 3. Logische Grundlage des Spiels

Unser Spiel besteht aus mehreren wichtigen Logikblöcken. Zuerst füllen wir das Spielfeld mit Diamanten. Dabei ist es wichtig sofort zu prüfen, dass keine Linien aus drei oder mehr gleichen Diamanten entstehen — so verhindern wir zufällige Matches beim Start.

Nachdem das Feld gefüllt ist, übergeben wir den Zug an den Spieler. Zur Vereinfachung kann der Spieler in diesem Stadium beliebige zwei benachbarte Diamanten vertikal oder horizontal tauschen, ohne auf Moves beschränkt zu sein, die direkt zu Matches führen.

Nach dem Zug des Spielers werden die Diamanten getauscht. Danach überprüfen wir das gesamte Feld auf Übereinstimmungen. Wenn Matches aus drei oder mehr gleichen Diamanten gefunden werden, entfernen wir diese und füllen die leeren Felder mit neuen Diamanten. Gibt es keine Matches, ist der Spieler wieder an der Reihe.

Schritt 4. Spiel-Logik steuern

Um die Hauptphasen des Spiels zu steuern, erstellen wir ein separates Skript. Erstelle ein neues C#-Skript (MonoBehaviour) und nenne es GameManager. Dieses Skript bestimmt die aktuelle Spielphase und startet die entsprechenden Logikblöcke. Außerdem speichert es Arrays für das Spielfeld und die Menge an Spielobjekten (z. B. Diamanten).

In diesem Stadium ist das Starten der Logikblöcke deaktiviert — wir verbinden sie später, wenn wir die jeweiligen Funktionen umgesetzt haben.


using UnityEngine;

public class GameManager : MonoBehaviour
{
    // Spielfeld-Array
    public GameObject[,] gemsGrid = new GameObject[6, 6];
    // Array mit Spielobjekten (Diamanten)
    public GameObject[] gemsArray;
    // Zähler für bewegende Diamanten
    public int gemCounter = 0;
    // Erlaubnis für Spielerzug
    public bool canMove = false;
    // Erlaubnis Matches zu prüfen
    public bool canCheck = false;
    // Erlaubnis das Feld neu zu füllen
    public bool canRefill = false;
    // Flag für keine Matches
    public bool noMatches = true;

    private void Update()
    {
        // Wenn keine Diamanten mehr in Bewegung sind und Matches geprüft werden können
        if (canCheck && gemCounter <= 0)
        {
            canCheck = false;
            gemCounter = 0;
            //GetComponent<GridChecker>().CheckForMatches();
        }

        // Wenn keine Diamanten mehr in Bewegung sind und das Feld gefüllt werden kann
        if (canRefill && gemCounter <= 0)
        {
            canRefill = false;
            gemCounter = 0;
            //GetComponent<RefillGrid>().Refill();
        }

        // Wenn keine Matches und keine anderen Prozesse — Zug an Spieler übergeben
        if (!canRefill && noMatches && !canCheck && gemCounter <= 0)
        {
            gemCounter = 0;
            canMove = true;
        }
    }
}
  

Jetzt erstelle ein leeres GameObject:

💡 GameObject → Create Empty

Benenne es um in GameManager und gib ihm den Tag GameManager.

💡 Falls es diesen Tag noch nicht gibt, füge ihn über das Tag-Menü hinzu.

Füge das Skript GameManager zu diesem Objekt hinzu. Wähle dazu das Objekt in der Hierarchie aus und ziehe das Skript ins Inspector-Feld oder nutze den Button Add Component.

GameManager-Objekt mit angehängtem GameManager-Skript
GameManager-Objekt mit angehängtem GameManager-Skript.

Schritt 5. Diamant-Template

Jetzt erstellen wir ein Template für die Diamanten unseres Spiels. Im zweiten Schritt hast du bereits das benötigte Sprite importiert. Ziehe es in die Szene — Unity erstellt automatisch ein neues GameObject.

In diesem Spiel werden wir die Diamanten mit transform.Translate bewegen und damit ihre Position im Raum ändern. Außerdem nutzen wir die integrierte 2D-Physik, um Interaktionen mit dem Spieler zu registrieren.

Einen Teil der Spiellogik platzieren wir direkt auf dem Diamanten-Objekt, damit wir kein separates Controller-Skript brauchen. Jeder Diamant speichert dafür selbst einige wichtige Informationen.

Füge zuerst dem Diamanten einen Circle Collider 2D-Komponenten hinzu, damit Kollisionsevents genutzt werden können.

Erstelle jetzt ein neues MonoBehaviour-Skript und nenne es GemData:


using UnityEngine;

public class GemData : MonoBehaviour
{
    // Position dieses Diamanten im Grid-Array,
    // entspricht der Position im Unity-Welt-Raum
    public int gridPosX, gridPosY;

    // Referenz auf den Collider dieses Diamanten
    private CircleCollider2D col;

    // Flag, ob Bewegung zur Zielposition erlaubt ist
    private bool canMoveToPos = false;

    // Bewegungsgeschwindigkeit
    public float speed;

    // Endpunkt, zu dem sich der Diamant bewegt
    private Vector3 endPos;

    // Referenz auf das Hauptspiel-Skript
    private GameManager manager;

    private void Awake()
    {
        // Finde das Objekt mit dem Tag GameManager und hole sein Skript
        manager = GameObject.FindWithTag("GameManager").GetComponent<GameManager>();
        // Hole diesen CircleCollider2D
        col = GetComponent<CircleCollider2D>();
    }

    private void Update()
    {
        // Wenn Bewegung erlaubt ist
        if (canMoveToPos)
        {
            // Berechne normalisierte Richtung zum Endpunkt
            Vector3 dir = (endPos - transform.position).normalized;
            // Bewege dich in diese Richtung mit der angegebenen Geschwindigkeit
            transform.Translate(dir * speed * Time.deltaTime);

            // Wenn fast am Zielpunkt angekommen
            if ((endPos - transform.position).sqrMagnitude < 0.05f)
            {
                // Setze exakt auf die Zielposition
                transform.position = endPos;

                // Collider wieder aktivieren, damit Kollisionen erkannt werden
                col.enabled = true;

                // Zähler für bewegende Diamanten im Manager verringern
                manager.gemCounter--;

                // Sortier-Reihenfolge wieder auf normal setzen
                GetComponent<SpriteRenderer>().sortingOrder = 0;

                // Bewegungs-Flag ausschalten
                canMoveToPos = false;
            }
        }
    }

    // Wird aufgerufen, um die Bewegung des Diamanten zu starten
    public void Move()
    {
        // Zielposition aus den Grid-Koordinaten aktualisieren
        endPos = new Vector3(gridPosX, gridPosY);

        // Collider deaktivieren, um während der Bewegung keine Kollisionen auszulösen
        col.enabled = false;

        // Zähler für bewegende Diamanten im Manager erhöhen
        manager.gemCounter++;

        // Höhere Sortier-Reihenfolge setzen, damit dieser Diamant oben gezeichnet wird
        GetComponent<SpriteRenderer>().sortingOrder = 10;

        // Bewegung erlauben
        canMoveToPos = true;
    }
}
  

Beachte dabei: Obwohl der Diamant einen 2D-Collider hat, verwenden wir für die Bewegung transform.Translate ohne Rigidbody2D. Das ist für einfache Logik schneller, erfordert aber, dass der Collider während der Bewegung manuell deaktiviert wird.

Beim Bewegen ändern wir außerdem die Sortier-Reihenfolge (sorting order), damit der Diamant über anderen Elementen gezeichnet wird. Sobald die Bewegung abgeschlossen ist, setzen wir sie zurück.

Zur Vereinfachung stimmen unsere Koordinaten im Array und in der Welt-Szene überein. Da in Unity standardmäßig eine Welteinheit 100 Pixel entspricht, nimmt ein Sprite von 100×100 Pixel genau eine Zelle ein. Ein Diamant an Position (1,4) im Array erscheint also genau an dieser Stelle in der Szene.

Ändere im Inspector die Bewegungsgeschwindigkeit speed auf 1 — später kannst du den optimalen Wert einstellen.

Fertig: Jetzt haben wir ein vollständiges Template für unsere Diamanten. Im nächsten Schritt fügen wir Variation hinzu, damit sie sich voneinander unterscheiden.

Diamant-Template mit angehängtem GemData-Skript
Diamant-Template mit angehängtem GemData-Skript.

Schritt 6. Erstellen der Diamant-Prefabs

Jetzt haben wir das Diamant-Template in der Szene und es ist Zeit, daraus echte Prefabs zu machen. Dabei ist es wichtig, das Gleichgewicht zu halten: Wenn es zu wenig Varianten gibt, entstehen Matches zu oft und der Spieler sieht nur, wie das Feld automatisch geleert und neu gefüllt wird (vielleicht ist das aber genau der Effekt, den du willst). Gibt es zu viele Varianten, werden Matches selten und das Spiel kann lange ohne mögliche Züge festhängen.

Für unser 6×6-Feld sind 4–5 Varianten optimal. Wir wählen fünf, um einen guten Mix aus Zufall und Spielerplanung zu erhalten.

Wähle den Diamanten in der Szene (oder im Hierarchie-Fenster) und benenne ihn um in Gem-Red.

💡 Der Name des Objekts ist für das Spiel nicht entscheidend, da wir uns an den Tags orientieren. Du kannst es also auch anders nennen, wenn du möchtest.

Erstelle einen neuen Tag red und weise ihn dem Objekt Gem-Red zu. Ändere danach in der Komponente Sprite Renderer die Farbe des Diamanten auf Rot. Ziehe jetzt das Objekt Gem-Red aus der Hierarchie in den Ordner Assets — Unity erstellt automatisch ein Prefab daraus.

Wähle danach erneut den Diamanten in der Szene und erstelle auf die gleiche Weise vier weitere Prefabs.

💡 Nach dem ersten Speichern arbeitest du mit einem Objekt, das mit diesem Prefab verbunden ist. Beim Erstellen des nächsten Prefabs fragt Unity, ob ein neues Original-Prefab oder eine Variante (Variant) erstellt werden soll. Wähle Original Prefab, um ein unabhängiges neues Prefab zu erhalten.

Am Ende habe ich diese Prefabs: Gem-Red mit dem Tag red, Gem-Blue mit dem Tag blue, Gem-Yellow mit dem Tag yellow, Gem-Green mit dem Tag green und Gem-Fuchsia mit dem Tag fuchsia. Die Farben stimmen jeweils überein.

Nachdem alle fünf Prefabs erstellt sind, kannst du das Diamant-Objekt aus der Szene löschen — alle benötigten Templates sind jetzt als Prefabs im Projekt gespeichert.

Fünf verschiedene Diamant-Prefabs
Fünf verschiedene Diamant-Prefabs.

Schritt 7. Erste Befüllung mit Diamanten

Der erste logische Schritt unseres Spiels ist, das Spielfeld mit zufälligen Diamanten zu füllen. Dafür bereiten wir zunächst ein Array vor, in dem die möglichen Diamant-Prefabs zum Spawnen gespeichert sind.

Wähle GameManager in der Hierarchie aus. Im Inspector findest du das Script GameManager, in dem sich das Array GemsArray befindet. Dieses ist aktuell leer. Ziehe deine fünf Diamant-Prefabs hinein.

💡 Du kannst auch einfach die Größe des Arrays auf 5 setzen und die benötigten Prefabs in den neu erschienenen Feldern aus dem Assets-Ordner auswählen.

Jetzt erstellen wir ein Script für die erste Befüllung des Spielfelds. Erstelle ein neues MonoBehaviour-Script und nenne es SpawnController. Füge es dem selben Objekt GameManager hinzu.


using UnityEngine;
// Für die Verwendung von List
using System.Collections.Generic;

public class SpawnController : MonoBehaviour
{
    private GameManager manager;

    void Start()
    {
        // Referenz auf GameManager holen
        manager = GetComponent<GameManager>();
        // Fülle das Spielfeld einmalig beim Start
        FillTheGrid();
    }

    public void FillTheGrid()
    {
        // Durchlaufe das Spielfeld auf X und Y
        for (int i = 0; i < 6; i++)
        {
            for (int j = 0; j < 6; j++)
            {
                Vector2 position = new Vector2(i, j);
                // Kopiere das Prefab-Array zur Filterung
                List<GameObject> candidateTypes = new List<GameObject>(manager.gemsArray);

                // Prüfe linken Nachbarn: falls vorhanden, entferne passenden Typ
                if (i >= 1)
                    candidateTypes.RemoveAll(gem => gem.tag == manager.gemsGrid[i - 1, j].tag);

                // Prüfe unteren Nachbarn: falls vorhanden, entferne passenden Typ
                if (j >= 1)
                    candidateTypes.RemoveAll(gem => gem.tag == manager.gemsGrid[i, j - 1].tag);

                // Wähle zufällig aus den verbleibenden Typen
                GameObject gem = Instantiate(
                    candidateTypes[Random.Range(0, candidateTypes.Count)],
                    position,
                    Quaternion.identity
                );

                // Setze die Position im Grid für den Diamanten
                GemData gemData = gem.GetComponent<GemData>();
                gemData.gridPosX = i;
                gemData.gridPosY = j;

                // Speichere die Referenz im Spielfeld-Array
                manager.gemsGrid[i, j] = gem;
            }
        }

        // Erlaube dem Spieler den ersten Zug
        manager.canMove = true;
    }
}
  

Dieses Script wird einmalig beim Start des Spiels ausgeführt. Es kopiert das Array der verfügbaren Diamant-Prefabs in eine dynamische Liste und filtert beim Durchlaufen jeder Zelle die Typen heraus, die bereits links oder unten angrenzen. So vermeiden wir Matches am Spielstart.

Anschließend wird zufällig ein Diamant aus den verbleibenden Kandidaten ausgewählt, im Szenenraum erstellt, seine Grid-Koordinaten werden gesetzt und er wird im Array gemsGrid gespeichert. Nach dem Befüllen ist das Spielfeld bereit und der Spieler kann den ersten Zug machen.

Zu diesem Zeitpunkt kannst du das Spiel starten und sehen, wie das Spielfeld mit zufälligen Diamanten gefüllt wird.

GameManager mit GemsArray-Array, gefüllt mit Prefabs
GameManager mit dem mit Prefabs gefüllten Array GemsArray.

Schritt 8. Bewegung der Diamanten

Nun hat der Spieler ein gefülltes Spielfeld vor sich und kann seinen ersten Zug machen. Dafür erstellen wir ein Skript, das die Maus-Eingaben und die Bewegung der ausgewählten Diamanten verarbeitet.

Erstelle ein neues MonoBehaviour-Skript mit dem Namen MoveGem. Füge dieses Skript dem Objekt GameManager hinzu.


using UnityEngine;

public class MoveGem : MonoBehaviour
{
    // Minimale Distanz zum Erkennen eines Swipes
    public float swipeThreshold = 0.5f;

    // Bewegungsrichtung als Text
    private string direction;

    // Ausgewählter Diamant
    private GameObject selectedGem;

    // Start- und Endpunkt der Maus
    private Vector2 startMouse;
    private Vector2 endMouse;

    private GameManager manager;

    void Start()
    {
        manager = GetComponent<GameManager>();
    }

    void Update()
    {
        // Wenn linke Maustaste gedrückt und Zug erlaubt ist
        if (Input.GetMouseButtonDown(0) && manager.canMove)
        {
            startMouse = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            RaycastHit2D hit = Physics2D.Raycast(startMouse, Vector2.zero);

            if (hit.collider != null)
            {
                // Gewählten Diamanten merken
                selectedGem = hit.collider.gameObject;
            }
        }

        // Wenn linke Maustaste losgelassen und ein Diamant gewählt wurde
        if (Input.GetMouseButtonUp(0) && selectedGem != null && manager.canMove)
        {
            endMouse = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            Vector2 delta = endMouse - startMouse;
            direction = "";

            // Prüfen, ob Swipe groß genug ist
            if (delta.magnitude > swipeThreshold)
            {
                // Richtung bestimmen
                if (Mathf.Abs(delta.x) >= Mathf.Abs(delta.y))
                    direction = delta.x > 0 ? "right" : "left";
                else
                    direction = delta.y > 0 ? "up" : "down";
            }

            if (direction != "")
            {
                // Diamanten tauschen starten
                SwitchGems(direction);
            }
        }
    }

    void SwitchGems(string direction)
    {
        switch (direction)
        {
            case "right": GemSwap(Vector2Int.right); break;
            case "left":  GemSwap(Vector2Int.left);  break;
            case "up":    GemSwap(Vector2Int.up);    break;
            case "down":  GemSwap(Vector2Int.down);  break;
        }
    }

    void GemSwap(Vector2Int dir)
    {
        int x1 = selectedGem.GetComponent<GemData>().gridPosX;
        int y1 = selectedGem.GetComponent<GemData>().gridPosY;
        int x2 = x1 + dir.x;
        int y2 = y1 + dir.y;

        // Spielfeldgrenzen prüfen
        if (x2 < 0 || x2 >= manager.gemsGrid.GetLength(0) || y2 < 0 || y2 >= manager.gemsGrid.GetLength(1))
            return;

        // Zwei Diamanten zum Tauschen auswählen
        GameObject gem1 = manager.gemsGrid[x1, y1];
        GameObject gem2 = manager.gemsGrid[x2, y2];

        var data1 = gem1.GetComponent<GemData>();
        var data2 = gem2.GetComponent<GemData>();

        // Koordinaten in den Daten aktualisieren
        data1.gridPosX = x2;
        data1.gridPosY = y2;
        data2.gridPosX = x1;
        data2.gridPosY = y1;

        // Im Array tauschen
        manager.gemsGrid[x1, y1] = gem2;
        manager.gemsGrid[x2, y2] = gem1;

        // Bewegung starten
        data1.Move();
        data2.Move();

        // Weitere Züge sperren, bis Matches geprüft sind
        manager.canMove = false;
        manager.canCheck = true;
    }
}
  

Dieses Skript registriert und verarbeitet Mausklicks. Wenn der Spieler die linke Maustaste drückt, wird der Startpunkt und der ausgewählte Diamant gespeichert. Beim Loslassen wird geprüft, wie weit der Cursor bewegt wurde — als Schutz gegen versehentliche Klicks. Ist die Bewegung groß genug, wird die Richtung bestimmt und eine Funktion zum Tauschen der Diamanten aufgerufen.

Dann finden wir das Diamantenpaar (den ausgewählten und den Nachbarn in der gewählten Richtung), tauschen deren Positionen im Array, aktualisieren die Koordinaten in den GemData-Skripten und schicken beide zu ihren neuen Positionen. Danach wird die Möglichkeit für weitere Züge deaktiviert, bis die Bewegung beendet und die Matches geprüft sind.

💡 Die Kamera ist vielleicht nicht zentriert auf das Spielfeld. Zur besseren Übersicht kannst du die Position auf x = 2.5 und y = 2.5 setzen, damit das Feld zentriert im Bildschirm ist.

Nun kannst du das Spiel starten und testen, wie die Diamanten die Plätze tauschen.

GameManager mit dem MoveGem-Skript
GameManager mit dem MoveGem-Skript.

Schritt 9. Spielfeld prüfen

Der nächste logische Schritt nach dem Zug des Spielers ist die Prüfung des Spielfelds auf Matches. Dafür erstellen wir ein neues MonoBehaviour-Script namens GridChecker. Füge dieses Script dem Objekt GameManager hinzu.


using UnityEngine;
using System.Collections.Generic;

public class GridChecker : MonoBehaviour
{
    // Array von Listen, in dem wir Gruppen von Matches (3+) speichern
    private List<List<GameObject>> matchedGems;
    private GameManager manager;
    GameObject[,] gemsGrid;

    public void CheckForMatches()
    {
        manager = GetComponent<GameManager>();
        gemsGrid = manager.gemsGrid;

        matchedGems = new List<List<GameObject>>();
        GameObject firstGemToCheck = null;
        // temporäre Liste zur Verarbeitung von Matches
        List<GameObject> tmpGems = new List<GameObject>();

        // Horizontale Überprüfung
        for (int y = 0; y < gemsGrid.GetLength(1); y++)
        {
            firstGemToCheck = null;
            tmpGems.Clear();

            for (int x = 0; x < gemsGrid.GetLength(0); x++)
            {
                GameObject current = gemsGrid[x, y];

                // Wenn leeres Feld gefunden, Unterbrechung der Iteration
                if (current == null)
                {
                    // Prüfen, ob bereits Matches gesammelt wurden
                    if (tmpGems.Count >= 3)
                        // und speichern
                        matchedGems.Add(new List<GameObject>(tmpGems));
                    tmpGems.Clear();
                    firstGemToCheck = null;
                    continue;
                }

                // Wenn erster Diamant in Reihe oder anderer Tag als vorheriger
                if (firstGemToCheck == null || current.tag != firstGemToCheck.tag)
                {
                    // Prüfen, ob vorherige Matches vorhanden sind
                    if (tmpGems.Count >= 3)
                        // speichern
                        matchedGems.Add(new List<GameObject>(tmpGems));
                    tmpGems.Clear();
                    // Neuen Diamanten zur Liste hinzufügen
                    tmpGems.Add(current);
                    firstGemToCheck = current;
                }
                else
                {
                    // Tag stimmt überein, Diamant zur aktuellen Kette hinzufügen
                    tmpGems.Add(current);
                }
            }

            // Wenn am Ende eine Serie von 3+ gleichartigen Diamanten besteht
            if (tmpGems.Count >= 3)
                // speichern
                matchedGems.Add(new List<GameObject>(tmpGems));
        }

        // Vertikale Überprüfung — nach dem gleichen Prinzip
        for (int x = 0; x < gemsGrid.GetLength(0); x++)
        {
            firstGemToCheck = null;
            tmpGems.Clear();

            for (int y = 0; y < gemsGrid.GetLength(1); y++)
            {
                GameObject current = gemsGrid[x, y];

                if (current == null)
                {
                    if (tmpGems.Count >= 3)
                        matchedGems.Add(new List<GameObject>(tmpGems));
                    tmpGems.Clear();
                    firstGemToCheck = null;
                    continue;
                }

                if (firstGemToCheck == null || current.tag != firstGemToCheck.tag)
                {
                    if (tmpGems.Count >= 3)
                        matchedGems.Add(new List<GameObject>(tmpGems));
                    tmpGems.Clear();
                    tmpGems.Add(current);
                    firstGemToCheck = current;
                }
                else
                {
                    tmpGems.Add(current);
                }
            }

            if (tmpGems.Count >= 3)
                matchedGems.Add(new List<GameObject>(tmpGems));
        }

        // Alle gefundenen Matches aus Grid und Szene löschen
        foreach (List<GameObject> group in matchedGems)
        {
            foreach (GameObject gem in group)
            {
                if (gem != null)
                {
                    GemData data = gem.GetComponent<GemData>();
                    manager.gemsGrid[data.gridPosX, data.gridPosY] = null;
                    Destroy(gem);
                }
            }
        }

        // Prüfen, ob Matches gefunden wurden
        bool hadMatches = matchedGems.Count > 0;
        matchedGems.Clear();

        // Wenn Matches gefunden, refill starten
        if (hadMatches)
        {
            manager.noMatches = false;
            manager.canRefill = true;
            manager.canCheck = false;
        }
        else
        {
            // Sonst Zug an Spieler zurückgeben über GameManager
            manager.noMatches = true;
            manager.canRefill = false;
            manager.canCheck = false;
        }
    }
}
  

Dieses Script überprüft Reihen und Spalten im Feld, um Gruppen von drei oder mehr gleichen Diamanten zu finden. Dabei wird eine temporäre Liste tmpGems genutzt, die aufeinanderfolgende gleiche Diamanten sammelt. Wenn ein anderer Typ oder eine leere Zelle gefunden wird, prüft das Script, ob mindestens drei gleiche Diamanten gesammelt sind und speichert diese dann in der Gesamtliste matchedGems.

Nach der Prüfung werden alle gefundenen Matches gelöscht, und je nachdem ob welche gefunden wurden, startet das Spiel das Auffüllen leerer Felder oder gibt den Zug an den Spieler zurück.

💡 Du kannst jetzt die Zeile GetComponent<GridChecker>().CheckForMatches(); im GameManager-Script auskommentieren, um die Match-Suche zu testen. Beachte, dass beim Interagieren mit leeren Feldern noch Fehler auftreten können — das wird später behoben.
GameManager mit dem GridChecker-Skript
GameManager mit dem GridChecker-Script.

Schritt 10. Auffüllen des Spielfelds

Jetzt steht der letzte logische Schritt des Spiels an. Auf dem Spielfeld sind nun leere Felder dort, wo vorher Diamanten gelöscht wurden. Wir müssen: diese leeren Stellen finden und die darüber liegenden Diamanten nach unten verschieben, und danach neue Diamanten für die verbleibenden leeren Felder spawnen.

Dafür erstellen wir ein neues MonoBehaviour-Script mit dem Namen RefillGrid. Dieses Script fügen wir dem GameManager hinzu.


using UnityEngine;
using System.Collections;

public class RefillGrid : MonoBehaviour
{
    private GameManager manager;
    private GameObject[,] gemsGrid;
    private GameObject[] gemsArray;

    private void Start()
    {
        manager = GetComponent<GameManager>();
        gemsArray = manager.gemsArray;
        gemsGrid = manager.gemsGrid;
    }

    public void Refill()
    {
        // Starte die Sequenz: Fallen -> Spawn -> Warten -> Überprüfung erlauben
        StartCoroutine(DoRefillSequence());
        manager.canRefill = false;
    }

    IEnumerator DoRefillSequence()
    {
        // Zuerst fallen die bestehenden Diamanten
        yield return StartCoroutine(SlowFall());
        // Dann spawnen neue an leeren Stellen
        yield return StartCoroutine(SlowSpawn());
        // Kurze Pause, damit Bewegungen abschließen können
        yield return new WaitForSeconds(0.2f);
        // Nun können Matches geprüft werden
        manager.canCheck = true;
    }

    IEnumerator SlowFall()
    {
        for (int x = 0; x < gemsGrid.GetLength(0); x++)
        {
            for (int y = 0; y < gemsGrid.GetLength(1) - 1; y++)
            {
                // Wenn leeres Feld in der Spalte gefunden wird
                if (gemsGrid[x, y] == null)
                {
                    // Suche darüber ersten Diamanten zum "fallen lassen"
                    for (int aboveY = y + 1; aboveY < gemsGrid.GetLength(1); aboveY++)
                    {
                        if (gemsGrid[x, aboveY] != null)
                        {
                            // Bewege gefundenen Diamanten ins leere Feld
                            GameObject gem = gemsGrid[x, aboveY];
                            gemsGrid[x, y] = gem;
                            gemsGrid[x, aboveY] = null;

                            // Aktualisiere Position im Skript und starte Bewegung
                            GemData data = gem.GetComponent<GemData>();
                            data.gridPosY = y;
                            data.Move();

                            // Pause, damit Fall sichtbar wird
                            yield return new WaitForSeconds(0.1f);
                            break;
                        }
                    }
                }
            }
        }
    }

    IEnumerator SlowSpawn()
    {
        for (int x = 0; x < gemsGrid.GetLength(0); x++)
        {
            for (int y = 0; y < gemsGrid.GetLength(1); y++)
            {
                // Wenn Feld nach Fallen noch leer ist — neuen Diamanten erzeugen
                if (gemsGrid[x, y] == null)
                {
                    // Erzeuge etwas oberhalb des Spielfelds
                    GameObject newGem = Instantiate(
                        gemsArray[Random.Range(0, gemsArray.Length)],
                        new Vector2(x, gemsGrid.GetLength(1) + 1),
                        Quaternion.identity
                    );

                    gemsGrid[x, y] = newGem;

                    // Setze Zielkoordinaten im Grid und starte Bewegung
                    GemData data = newGem.GetComponent<GemData>();
                    data.gridPosX = x;
                    data.gridPosY = y;
                    data.Move();

                    // Verzögerung für visuelle Darstellung
                    yield return new WaitForSeconds(0.2f);
                }
            }
        }
    }
}
  

Dieses Script erfüllt zwei wichtige Aufgaben: Es sucht zuerst leere Felder und verschiebt die Diamanten von oben nach unten. Danach findet es die verbleibenden leeren Felder und spawnt dort neue Diamanten.

Damit ist unsere Basis-Spiel-Logik fertig! Nun kannst du die Aufrufe GetComponent<GridChecker>().CheckForMatches() und GetComponent<RefillGrid>().Refill() im Script GameManager auskommentieren, und dein Spiel läuft im kompletten Zyklus.

GameManager mit dem RefillGrid-Script
GameManager mit dem RefillGrid-Script.

Schritt 11. Lass deiner Fantasie freien Lauf

Ich hoffe, du hast nicht vergessen, die Aufrufe CheckForMatches() und Refill() im Script GameManager zu aktivieren — nun läuft dein Spiel vollständig!

Von hier an liegt alles an deiner Fantasie 🤗 Füge Spezialeffekte, Animationen, Sounds, Boni, Timer und Levels hinzu — und mach dein Match-3-Spiel einzigartig.