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 MonoBehaviour-Skript 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
{
    // 2D-Array, das das Spielfeld darstellt (6×6 Felder)
    public GameObject[,] gemsGrid = new GameObject[6, 6];

    // Array mit verfügbaren Edelstein-Prefabs (verschiedene Farben)
    public GameObject[] gemsArray;

    // Zähler für Edelsteine, die sich gerade bewegen
    public int gemCounter = 0;

    // Flag: erlaubt dem Spieler, einen Zug zu machen
    public bool canMove = false;

    // Flag: löst die Überprüfung auf Übereinstimmungen aus
    public bool canCheck = false;

    // Flag: löst das Auffüllen des Spielfelds nach Löschungen aus
    public bool canRefill = false;

    // Flag: zeigt an, ob aktuell keine Übereinstimmungen vorhanden sind
    public bool noMatches = true;

    private void Update()
    {
        // Wenn sich keine Edelsteine mehr bewegen und Prüfung erlaubt ist — prüfe auf Matches
        if (canCheck && gemCounter <= 0)
        {
            canCheck = false;
            gemCounter = 0;
            //GetComponent<GridChecker>().CheckForMatches();
        }

        // Wenn sich keine Edelsteine mehr bewegen und Auffüllen erlaubt ist — fülle das Feld
        if (canRefill && gemCounter <= 0)
        {
            canRefill = false;
            gemCounter = 0;
            //GetComponent<RefillGrid>().Refill();
        }

        // Wenn keine Prozesse mehr laufen und keine Übereinstimmungen gefunden wurden — Spieler darf ziehen
        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 Edelsteins im Gitter-Array,
    // entspricht seiner Weltposition in Unity
    public int gridPosX, gridPosY;

    // Referenz auf den CircleCollider2D dieses Edelsteins
    private CircleCollider2D col;

    // Flag: erlaubt Bewegung zur Zielposition
    private bool canMoveToPos = false;

    // Bewegungsgeschwindigkeit
    public float speed;

    // Zielposition, zu der sich der Edelstein bewegen soll
    private Vector3 endPos;

    // Referenz auf das Hauptskript GameManager
    private GameManager manager;

    private void Awake()
    {
        // Suche nach dem Objekt mit dem Tag "GameManager" und hole sein Skript
        manager = GameObject.FindWithTag("GameManager").GetComponent<GameManager>();

        // Hole die eigene CircleCollider2D-Komponente
        col = GetComponent<CircleCollider2D>();
    }

    private void Update()
    {
        // Wenn Bewegung erlaubt ist — bewege den Edelstein
        if (canMoveToPos)
        {
            // Berechne die Richtung zur Zielposition (normalisiert)
            Vector3 dir = (endPos - transform.position).normalized;

            // Bewege den Edelstein in diese Richtung mit definierter Geschwindigkeit
            transform.Translate(dir * speed * Time.deltaTime);

            // Wenn das Ziel fast erreicht ist
            if ((endPos - transform.position).sqrMagnitude < 0.05f)
            {
                // Setze präzise auf die Zielposition, um Ungenauigkeiten zu vermeiden
                transform.position = endPos;

                // Aktiviere den Collider wieder — Kollisionen sind jetzt wieder möglich
                col.enabled = true;

                // Informiere den GameManager, dass ein Edelstein fertig bewegt wurde
                manager.gemCounter--;

                // Setze die Zeichenreihenfolge zurück auf normal
                GetComponent<SpriteRenderer>().sortingOrder = 0;

                // Bewegung beenden
                canMoveToPos = false;
            }
        }
    }

    // Methode zum Starten der Bewegung zur Zielposition
    public void Move()
    {
        // Berechne Weltposition basierend auf Gitterkoordinaten
        endPos = new Vector3(gridPosX, gridPosY);

        // Deaktiviere Collider während der Bewegung, um Kollisionen zu vermeiden
        col.enabled = false;

        // Erhöhe Zähler für aktive Bewegungen im GameManager
        manager.gemCounter++;

        // Setze die Zeichenreihenfolge höher, damit dieser Edelstein oben angezeigt wird
        GetComponent<SpriteRenderer>().sortingOrder = 10;

        // Bewegung aktivieren
        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 das GameManager-Skript auf demselben Objekt holen
        manager = GetComponent<GameManager>();

        // Beim Start des Spiels das Spielfeld mit Edelsteinen füllen
        FillTheGrid();
    }

    public void FillTheGrid()
    {
        // Schleife über jedes Feld im Gitter anhand von X- und Y-Koordinaten
        for (int i = 0; i < 6; i++)
        {
            for (int j = 0; j < 6; j++)
            {
                Vector2 position = new Vector2(i, j);

                // Kopie des Prefab-Arrays erstellen, um Filterung zu ermöglichen
                List<GameObject> candidateTypes = new List<GameObject>(manager.gemsArray);

                // Linken Nachbarn prüfen – gleichen Typ entfernen
                if (i >= 1)
                    candidateTypes.RemoveAll(gem => gem.tag == manager.gemsGrid[i - 1, j].tag);

                // Unteren Nachbarn prüfen – gleichen Typ entfernen
                if (j >= 1)
                    candidateTypes.RemoveAll(gem => gem.tag == manager.gemsGrid[i, j - 1].tag);

                // Zufälliges Prefab aus den verbleibenden auswählen
                GameObject gem = Instantiate(
                    candidateTypes[Random.Range(0, candidateTypes.Count)],
                    position,
                    Quaternion.identity
                );

                // Koordinaten im Gitter dem Edelstein zuweisen
                GemData gemData = gem.GetComponent<GemData>();
                gemData.gridPosX = i;
                gemData.gridPosY = j;

                // Edelstein im Spielfeld-Array speichern
                manager.gemsGrid[i, j] = gem;
            }
        }

        // Spieler darf den ersten Zug machen
        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 Wischstrecke, um eine Bewegung auszulösen (in Welt-Einheiten)
    public float swipeThreshold = 0.5f;

    // Bewegungsrichtung als Text ("left", "right", "up", "down")
    private string direction;

    // Der vom Spieler ausgewählte Edelstein
    private GameObject selectedGem;

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

    private GameManager manager;

    void Start()
    {
        // Referenz auf den GameManager im selben Objekt holen
        manager = GetComponent<GameManager>();
    }

    void Update()
    {
        // Mausklick erkannt — Wischbewegung beginnt
        if (Input.GetMouseButtonDown(0) && manager.canMove)
        {
            // Startposition der Maus in Weltkoordinaten speichern
            startMouse = Camera.main.ScreenToWorldPoint(Input.mousePosition);

            // Prüfen, ob etwas getroffen wurde
            RaycastHit2D hit = Physics2D.Raycast(startMouse, Vector2.zero);
            if (hit.collider != null)
            {
                // Ausgewählten Edelstein speichern
                selectedGem = hit.collider.gameObject;
            }
        }

        // Maustaste losgelassen — Wischbewegung endet
        if (Input.GetMouseButtonUp(0) && selectedGem != null && manager.canMove)
        {
            endMouse = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            Vector2 delta = endMouse - startMouse;
            direction = "";

            // Wischstrecke prüfen
            if (delta.magnitude > swipeThreshold)
            {
                // Richtung anhand der dominanten Achse bestimmen
                if (Mathf.Abs(delta.x) >= Mathf.Abs(delta.y))
                    direction = delta.x > 0 ? "right" : "left";
                else
                    direction = delta.y > 0 ? "up" : "down";
            }

            // Wenn Richtung erkannt — Edelsteine tauschen
            if (direction != "")
            {
                SwitchGems(direction);
            }
        }
    }

    // Wandelt Richtungsstring in konkrete Bewegung um
    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;
        }
    }

    // Tauscht zwei benachbarte Edelsteine
    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;

        GameObject gem1 = manager.gemsGrid[x1, y1];
        GameObject gem2 = manager.gemsGrid[x2, y2];

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

        // Gitterkoordinaten 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 Eingaben vorübergehend deaktivieren
        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;
// Für Listen (dynamische Arrays)
using System.Collections.Generic;

// Klasse zum Überprüfen des Gitters auf Übereinstimmungen (3 oder mehr gleiche Edelsteine)
public class GridChecker : MonoBehaviour
{
    // Liste aller Gruppen von passenden Edelsteinen
    private List<List<GameObject>> matchedGems;

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

    // Zwischengespeichertes Edelstein-Gitter
    private GameObject[,] gemsGrid;

    // Hauptmethode zum Finden und Entfernen von Übereinstimmungen
    public void CheckForMatches()
    {
        // Hole GameManager und das Gitter
        manager = GetComponent<GameManager>();
        gemsGrid = manager.gemsGrid;

        // Neue Liste für alle gefundenen Kombinationen erstellen
        matchedGems = new List<List<GameObject>>();

        // Erstes Objekt zum Vergleich in der Reihe
        GameObject firstGemToCheck = null;

        // Temporäre Liste für Übereinstimmungskette
        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++)
            {
                // Aktuellen Edelstein holen
                GameObject current = gemsGrid[x, y];

                // Wenn leer — vorherige Kette ggf. speichern
                if (current == null)
                {
                    if (tmpGems.Count >= 3)
                        matchedGems.Add(new List<GameObject>(tmpGems));
                    tmpGems.Clear();
                    firstGemToCheck = null;
                    continue;
                }

                // Neuer Typ oder erstes Element
                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
                {
                    // Gleicher Typ — Kette erweitern
                    tmpGems.Add(current);
                }
            }

            // Letzte Kette in der Reihe prüfen
            if (tmpGems.Count >= 3)
                matchedGems.Add(new List<GameObject>(tmpGems));
        }

        // === Vertikale Überprüfung ===
        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);
                }
            }

            // Letzte vertikale Kette prüfen
            if (tmpGems.Count >= 3)
                matchedGems.Add(new List<GameObject>(tmpGems));
        }

        // === Alle passenden Edelsteine entfernen ===
        foreach (List<GameObject> group in matchedGems)
        {
            foreach (GameObject gem in group)
            {
                if (gem != null)
                {
                    // Aus dem Gitter entfernen
                    GemData data = gem.GetComponent<GemData>();
                    manager.gemsGrid[data.gridPosX, data.gridPosY] = null;

                    // Objekt in der Szene zerstören
                    Destroy(gem);
                }
            }
        }

        // Prüfen, ob Übereinstimmungen vorhanden waren
        bool hadMatches = matchedGems.Count > 0;

        // Liste bereinigen
        matchedGems.Clear();

        // === Spielstatus aktualisieren ===
        if (hadMatches)
        {
            manager.noMatches = false;
            manager.canRefill = true;
            manager.canCheck = false;
        }
        else
        {
            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;
// Für Coroutine-Unterstützung
using System.Collections;

public class RefillGrid : MonoBehaviour
{
    // Referenz auf den Hauptspielmanager
    private GameManager manager;

    // Das Spielfeld mit allen Edelsteinen
    private GameObject[,] gemsGrid;

    // Array mit möglichen Edelstein-Prefabs
    private GameObject[] gemsArray;

    private void Start()
    {
        // Referenzen aus dem GameManager holen
        manager = GetComponent<GameManager>();
        gemsArray = manager.gemsArray;
        gemsGrid = manager.gemsGrid;
    }

    // Startet den Auffüllvorgang
    public void Refill()
    {
        // Startet die Ablauf-Reihenfolge: Fallen → Erzeugen → Prüfen
        StartCoroutine(DoRefillSequence());

        // Deaktiviert das Nachfüll-Flag bis alles fertig ist
        manager.canRefill = false;
    }

    // Ablauf von Fall- und Erzeugungsprozess
    IEnumerator DoRefillSequence()
    {
        // Bestehende Edelsteine fallen lassen
        yield return StartCoroutine(SlowFall());

        // Neue Edelsteine erzeugen
        yield return StartCoroutine(SlowSpawn());

        // Kurze Pause für Animationen
        yield return new WaitForSeconds(0.2f);

        // Jetzt darf nach Übereinstimmungen gesucht werden
        manager.canCheck = true;
    }

    // Lässt Edelsteine in leere Zellen fallen
    IEnumerator SlowFall()
    {
        for (int x = 0; x < gemsGrid.GetLength(0); x++)
        {
            for (int y = 0; y < gemsGrid.GetLength(1) - 1; y++)
            {
                // Wenn die Zelle leer ist
                if (gemsGrid[x, y] == null)
                {
                    // Suche nach einem Edelstein weiter oben
                    for (int aboveY = y + 1; aboveY < gemsGrid.GetLength(1); aboveY++)
                    {
                        if (gemsGrid[x, aboveY] != null)
                        {
                            // Bewege den Edelstein nach unten
                            GameObject gem = gemsGrid[x, aboveY];
                            gemsGrid[x, y] = gem;
                            gemsGrid[x, aboveY] = null;

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

                            // Kurze Pause für Schritt-für-Schritt-Fallanimation
                            yield return new WaitForSeconds(0.1f);
                            break;
                        }
                    }
                }
            }
        }
    }

    // Neue Edelsteine in leere Felder erzeugen
    IEnumerator SlowSpawn()
    {
        for (int x = 0; x < gemsGrid.GetLength(0); x++)
        {
            for (int y = 0; y < gemsGrid.GetLength(1); y++)
            {
                // Wenn nach dem Fall noch leer
                if (gemsGrid[x, y] == null)
                {
                    // Spawne leicht oberhalb des Spielfeldes
                    GameObject newGem = Instantiate(
                        gemsArray[Random.Range(0, gemsArray.Length)],
                        new Vector2(x, gemsGrid.GetLength(1) + 1),
                        Quaternion.identity
                    );

                    // In das Spielfeld eintragen
                    gemsGrid[x, y] = newGem;

                    // Zielposition setzen und Bewegung starten
                    GemData data = newGem.GetComponent<GemData>();
                    data.gridPosX = x;
                    data.gridPosY = y;
                    data.Move();

                    // Pause für sichtbare Schritt-für-Schritt-Spawns
                    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.