Unity’de 2D Tetris: Parçalar ve Satır Temizleme Temelleri

Unity'de 2D Tetris oyunu geliştirmenin temellerini öğrenin. Parça oluşturma, hareket ettirme, döndürme ve satır temizleme mekaniklerini adım adım keşfedin.

2D oyun geliştirme dünyasına adım atmak isteyenler için Tetris, hem eğlenceli hem de öğretici bir başlangıç projesidir. Bu makalede, Unity Tetris geliştirme sürecinde parçaların (tetromino) yönetimi, hareket ettirilmesi, döndürülmesi ve temel satır temizleme mekaniklerini adım adım inceleyeceğiz. Bu rehber, oyunun çekirdek mantığını anlamanıza ve kendi Tetris klonunuzu oluşturmanıza yardımcı olacaktır.

Tetris’in Temel Mekanikleri ve Oyun Alanı Kurulumu

Tetris, düşen blokları belirli bir oyun alanında (grid) düzenleyerek tam satırlar oluşturmayı ve bu satırları temizlemeyi amaçlayan klasik bir bulmaca oyunudur. Oyunun ana bileşenleri şunlardır:

  • Oyun Alanı (Grid): Blokların düştüğü ve istiflendiği sabit bir ızgara yapısı. Genellikle 10×20 birimdir.
  • Tetromino’lar: Dört kareden oluşan ve farklı şekillerde gruplanmış parçalar (I, J, L, O, S, T, Z).
  • Hareket ve Döndürme: Düşen parçayı sola, sağa hareket ettirme ve döndürme yeteneği.
  • Çarpışma Algılama: Parçaların diğer bloklarla veya oyun alanı sınırlarıyla çarpışmasını algılama.
  • Satır Temizleme: Tamamlanmış satırları algılayıp temizleme ve üstteki blokları aşağı kaydırma.

Oyun Alanı (Grid) Yönetimi

Oyun alanını yönetmek, Tetris’in temelidir. Unity’de bunu bir 2D `GameObject` dizisi veya `Transform` dizisi olarak temsil edebiliriz. Bu dizi, oyun alanındaki her bir hücrenin dolu mu boş mu olduğunu ve doluysa hangi bloğu içerdiğini tutacaktır. Böylece, parçaların nereye düşebileceğini, nereye yerleştiğini ve hangi satırların tamamlandığını kolayca kontrol edebiliriz.

Öncelikle, oyun alanımızın boyutlarını ve her bir hücrenin referansını tutacak bir `GridManager` script’i oluşturalım. Bu script, oyun alanının merkezini ve boyutlarını ayarlayarak, parça hareketleri ve satır temizleme için temel bir referans noktası sağlayacaktır. Unity Tetris geliştirme sürecinde bu yapı, oyunun iskeletini oluşturur.


using UnityEngine;

public class GridManager : MonoBehaviour
{
    public static int width = 10; // Oyun alanı genişliği
    public static int height = 20; // Oyun alanı yüksekliği

    public static Transform[,] grid = new Transform[width, height];

    // Belirli bir pozisyonun oyun alanı içinde olup olmadığını kontrol eder
    public static bool IsInsideBorder(Vector2 pos)
    {
        return ((int)pos.x >= 0 && (int)pos.x < width && (int)pos.y >= 0);
    }

    // Belirli bir pozisyonun oyun alanı içinde ve boş olup olmadığını kontrol eder
    public static bool IsValidGridPos(Vector2 pos)
    {
        return IsInsideBorder(pos) && (grid[(int)pos.x, (int)pos.y] == null);
    }

    // Oyun alanına bir bloğu ekler
    public static void AddToGrid(Piece piece)
    {
        foreach (Transform child in piece.transform)
        {
            Vector2 pos = RoundVec2(child.position);
            if (IsInsideBorder(pos)) // Sadece sınırlar içindeki blokları ekle
            {
                grid[(int)pos.x, (int)pos.y] = child;
            }
        }
    }

    // Vektör2'yi tam sayıya yuvarlar
    public static Vector2 RoundVec2(Vector2 v)
    {
        return new Vector2(Mathf.Round(v.x), Mathf.Round(v.y));
    }

    // ... Diğer metodlar buraya gelecek (satır temizleme vb.)
}

Bu `GridManager` script’i, parçaların oyun alanı sınırları içinde olup olmadığını ve belirli bir pozisyonun boş olup olmadığını kontrol etmemizi sağlar. `AddToGrid` metodu ise bir parça yere oturduğunda, parçayı oluşturan her bir bloğun referansını `grid` dizimize ekler.

Tetris Parçalarının (Tetromino) Yönetimi

Her bir Tetris parçasını bir GameObject olarak düşünebiliriz. Bu GameObject, parçayı oluşturan küçük kare blokları (genellikle Sprite renderer’lı basit kareler) çocuk nesneler olarak içerecektir. Her bir Tetris parçası için bir Prefab oluşturmak, oyunu daha modüler ve yönetilebilir hale getirir.


using UnityEngine;

public class Piece : MonoBehaviour
{
    public Vector3 rotationPoint; // Dönme noktası
    private float fallTime = 0.8f; // Düşme hızı
    private float previousFallTime;

    void Start()
    {
        previousFallTime = Time.time;
    }

    void Update()
    {
        // Kullanıcı girdileri
        if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            transform.position += new Vector3(-1, 0, 0);
            if (!IsValidPosition()) // Geçersizse geri al
                transform.position += new Vector3(1, 0, 0);
        }
        else if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            transform.position += new Vector3(1, 0, 0);
            if (!IsValidPosition())
                transform.position += new Vector3(-1, 0, 0);
        }
        else if (Input.GetKeyDown(KeyCode.UpArrow))
        {
            transform.RotateAround(transform.TransformPoint(rotationPoint), new Vector3(0, 0, 1), 90);
            if (!IsValidPosition())
                transform.RotateAround(transform.TransformPoint(rotationPoint), new Vector3(0, 0, 1), -90);
        }

        // Otomatik düşme
        if (Time.time - previousFallTime > fallTime)
        {
            transform.position += new Vector3(0, -1, 0);
            if (!IsValidPosition()) // Eğer düşemiyorsa
            {
                transform.position += new Vector3(0, 1, 0); // Eski pozisyona geri al
                GridManager.AddToGrid(this); // Oyun alanına ekle
                GridManager.CheckForFullRows(); // Satır kontrolü yap
                FindObjectOfType().SpawnNext(); // Yeni parça oluştur
                enabled = false; // Bu parçanın script'ini devre dışı bırak
            }
            previousFallTime = Time.time;
        }

        // Hızlı düşme (Soft Drop)
        if (Input.GetKey(KeyCode.DownArrow))
        {
            fallTime = 0.05f; // Hızlı düşme hızı
        }
        else
        {
            fallTime = 0.8f; // Normal düşme hızı
        }
    }

    bool IsValidPosition()
    {
        foreach (Transform child in transform)
        {
            Vector2 v = GridManager.RoundVec2(child.position);
            if (!GridManager.IsInsideBorder(v))
                return false;
            if (GridManager.grid[(int)v.x, (int)v.y] != null && GridManager.grid[(int)v.x, (int)v.y].parent != transform)
                return false; // Başka bir parçaya çarpma kontrolü
        }
        return true;
    }
}

Bu Piece script’i, bir tetromino’nun hareketini, döndürülmesini ve otomatik düşmesini yönetir. IsValidPosition() metodu, parçanın yeni bir konuma veya dönme açısına geçmeden önce geçerli olup olmadığını kontrol eder. Eğer parça daha fazla düşemez hale gelirse, `GridManager.AddToGrid()` ile oyun alanına eklenir, ardından satır temizleme kontrolü yapılır ve yeni bir parça oluşturulur. Bu, Unity Tetris geliştirme sürecinde parçaların dinamik davranışını sağlayan temel mekanizmadır.

Satır Temizleme Mekaniği

Bir parça yere oturduğunda veya başka bir parçanın üzerine düştüğünde, oyun alanında tam dolu satır olup olmadığını kontrol etmemiz gerekir. Eğer tam dolu bir satır bulunursa, bu satır temizlenmeli ve üstündeki tüm bloklar bir birim aşağı kaydırılmalıdır. Bu, Tetris’in puan kazanma ve oyun alanını boşaltma döngüsünün kritik bir parçasıdır.


// GridManager.cs içerisine eklenecek metodlar

    // Tüm satırları kontrol eder ve tam dolu olanları temizler
    public static void CheckForFullRows()
    {
        for (int i = height - 1; i >= 0; i--)
        {
            if (IsRowFull(i))
            {
                DeleteRow(i);
                DecreaseRowsAbove(i + 1);
            }
        }
    }

    // Belirli bir satırın dolu olup olmadığını kontrol eder
    public static bool IsRowFull(int y)
    {
        for (int x = 0; x < width; x++)
        {
            if (grid[x, y] == null)
                return false;
        }
        return true;
    }

    // Belirli bir satırı temizler
    public static void DeleteRow(int y)
    {
        for (int x = 0; x < width; x++)
        {
            Destroy(grid[x, y].gameObject); // Bloğu yok et
            grid[x, y] = null; // Grid'den referansı kaldır
        }
    }

    // Belirli bir satırın üzerindeki tüm satırları aşağı kaydırır
    public static void DecreaseRowsAbove(int y)
    {
        for (int i = y; i < height; i++)
        {
            DecreaseRow(i);
        }
    }

    // Belirli bir satırdaki tüm blokları bir birim aşağı kaydırır
    public static void DecreaseRow(int y)
    {
        for (int x = 0; x < width; x++)
        {
            if (grid[x, y] != null)
            {
                // Bloğu bir birim aşağı taşı
                grid[x, y - 1] = grid[x, y];
                grid[x, y] = null;

                // Unity'deki objeyi de taşı
                grid[x, y - 1].position += new Vector3(0, -1, 0);
            }
        }
    }

CheckForFullRows() metodu, oyun alanındaki her satırı yukarıdan aşağıya doğru kontrol eder. Eğer bir satır tamamen doluysa (IsRowFull()), bu satırdaki tüm bloklar yok edilir (DeleteRow()) ve bu satırın üzerindeki tüm bloklar bir birim aşağı kaydırılır (DecreaseRowsAbove() ve DecreaseRow()). Bu döngü, birden fazla satırın aynı anda temizlenmesine de olanak tanır. Bu mekanikler, Unity Tetris geliştirme projenizin çekirdeğini oluşturur.

Pratik İpuçları

  1. Hayalet Parça (Ghost Piece): Oyuncunun parçayı nereye bırakacağını görmesini sağlayan, şeffaf bir ‘hayalet’ parça ekleyin. Bu, oyuncuya stratejik karar vermesi için büyük kolaylık sağlar. Mevcut parçanın en alt düşebileceği noktayı hesaplayarak ve o pozisyonda şeffaf bir kopya oluşturarak bunu yapabilirsiniz.
  2. Sert Bırakma (Hard Drop): Parçayı anında en alt pozisyona düşüren bir ‘sert bırakma’ (genellikle boşluk tuşu) özelliği ekleyin. Bu, IsValidPosition() metodunu kullanarak parçanın en alt geçerli pozisyonunu hızlıca bulup oraya taşıyarak uygulanabilir.
  3. Sonraki Parça Önizlemesi: Oyuncuya bir sonraki düşecek parçayı gösteren küçük bir pencere ekleyin. Bu, oyuncunun bir sonraki hamlesini planlamasına yardımcı olur. Bir Spawner script’i içinde sonraki parçayı saklayıp UI’da göstermek yeterlidir.

Yaygın Hatalar ve Çözümleri

  • Off-by-One Hataları: Grid dizisiyle çalışırken (0’dan width-1‘e kadar), dizinin sınırlarını aşan hatalar sıkça yaşanır. Özellikle döngülerde < yerine <= kullanmak veya tam tersi hatalara yol açabilir. Sınır kontrollerini her zaman dikkatlice yapın.
  • Dönme Çarpışma Hataları: Parçayı döndürdüğünüzde, dönme noktasının yanlış ayarlanması veya dönme sonrası geçersiz bir pozisyona düşülmesi yaygın bir sorundur. Dönme öncesi ve sonrası IsValidPosition() kontrolünü doğru bir şekilde uyguladığınızdan emin olun. Özellikle duvar ‘tekmeleme’ (wall kick) mekaniklerini araştırarak daha gelişmiş dönme algoritmaları uygulayabilirsiniz.
  • Performans Sorunları (Çok Fazla GameObject): Her bir küçük kare blok için ayrı bir GameObject kullanmak, özellikle oyun alanı dolduğunda performans düşüşlerine neden olabilir. Bu, özellikle mobil platformlarda veya büyük gridlerde belirginleşir.

Performans ve Optimizasyon Notları

Performans açısından, Tetris gibi oyunlarda `GameObject` sayısını yönetmek önemlidir. Her bir küçük bloğu ayrı bir `GameObject` olarak tutmak yerine, bir Object Pooling sistemi kullanmayı düşünebilirsiniz. Bu, blokları yok edip tekrar oluşturmak yerine, onları devre dışı bırakıp tekrar kullanıma sokarak bellek ayırma ve yok etme maliyetlerini azaltır.

Ayrıca, `CheckForFullRows()` gibi sık çalışan metotlarınızı optimize etmek için gereksiz döngüleri veya pahalı işlemleri minimize etmeye çalışın. Örneğin, sadece değişen satırları kontrol etmek veya satır temizleme sırasında blokları tek tek yok etmek yerine toplu işlemler yapmak performansınızı artırabilir. Unity Tetris geliştirme projenizin ölçeğine göre bu optimizasyonlar daha da önemli hale gelecektir.

Bu makalede, Unity’de 2D Tetris oyununun temel parçalarını, yani parça hareketini, döndürmeyi, oyun alanına yerleştirmeyi ve satır temizleme mekaniğini detaylı bir şekilde ele aldık. Bu temelleri kullanarak kendi Tetris oyununuzu geliştirmeye başlayabilir ve üzerine ek özellikler inşa edebilirsiniz. Başarılar!

Leave a Reply

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir