Unity Eğitim
Dersler
Forum Sınav Merkezi Premium 💎
Bulmaca Oyunları

Unity ile Sokoban: Kutu İtme Mekaniklerini Uygulama

Paylaşan: Unity Eğitim 05 November 2025 10 dakika okuma 11 görüntülenme

Giriş: Sokoban'ın Büyüsü ve Mekanikleri

Sokoban, Japonca'da 'ambar görevlisi' anlamına gelen, basit ama derin bir bulmaca oyunudur. Amaç, bir ambar içindeki tüm kutuları belirli hedef noktalarına iterek yerleştirmektir. Bu oyunun temel çekiciliği, oyuncunun kutuları sadece itebilmesi (çekememesi) ve aynı anda birden fazla kutuyu itememesidir. Bu kısıtlamalar, her hamlenin dikkatlice planlanmasını gerektiren karmaşık bulmacalar ortaya çıkarır. Unity ile kendi Sokoban benzeri oyununuzu geliştirmek, hem oyun mantığı hem de grid tabanlı hareket sistemleri hakkında değerli bilgiler edinmenizi sağlar. Bu makalede, bir Sokoban oyununun temel kutu itme mekanikleri üzerinde duracak ve bu sistemleri Unity/C# kullanarak nasıl hayata geçirebileceğinizi adım adım inceleyeceğiz.

Sokoban Mekaniklerinin Temelleri: Grid Sistemi ve Hareket

Sokoban gibi oyunlar, genellikle bir grid (ızgara) sistemi üzerinde çalışır. Bu, oyun dünyasının karelere bölünmüş bir yapıya sahip olduğu anlamına gelir. Oyuncu, kutular ve duvarlar bu grid üzerindeki belirli hücreleri işgal eder. Bu tür bir yapılandırma, hareket ve çarpışma mantığını basitleştirir, çünkü her şey tam sayılarla temsil edilen koordinatlara dayanır.

Unity'de bu grid yapısını temsil etmek için çeşitli yollar vardır. En yaygın ve etkili yöntemlerden biri, iki boyutlu bir dizi (char[,], int[,] veya GameObject[,]) kullanmaktır. Örneğin, bir char[,] dizisi ile 'W' duvarı, 'P' oyuncuyu, 'B' kutuyu ve ' ' (boşluk) boş bir hücreyi temsil edebiliriz. Bu, oyunun mevcut durumunu hızlıca sorgulamamızı sağlar.

Oyuncu hareketi, klavye girdileriyle tetiklenir ve her zaman tek bir grid birimi kadar gerçekleşir. Örneğin, 'W' tuşuna basıldığında oyuncu yukarı, 'A' tuşuna basıldığında sola hareket etmeye çalışır. Bu hareketin geçerli olup olmadığını anlamak için, oyuncunun hedeflediği hücredeki içeriği kontrol etmemiz gerekir. İşte bu noktada Sokoban mekanikleri devreye girer.

public class PlayerMovement : MonoBehaviour
{
    public float moveSpeed = 5f;
    private bool isMoving;
    private Vector2 targetPosition;
    private LevelManager levelManager; // Grid verilerini yönetecek sınıf

    void Start()
    {
        levelManager = FindObjectOfType<LevelManager>(); // Veya daha güvenli bir yöntemle al
        targetPosition = transform.position;
    }

    void Update()
    {
        if (!isMoving)
        {
            Vector2 input = Vector2.zero;
            if (Input.GetKeyDown(KeyCode.W)) input = Vector2.up;
            else if (Input.GetKeyDown(KeyCode.S)) input = Vector2.down;
            else if (Input.GetKeyDown(KeyCode.A)) input = Vector2.left;
            else if (Input.GetKeyDown(KeyCode.D)) input = Vector2.right;

            if (input != Vector2.zero)
            {
                TryMove(input);
            }
        }
        MoveSmoothly();
    }

    void TryMove(Vector2 direction)
    {
        Vector2Int currentGridPos = levelManager.WorldToGrid(transform.position);
        Vector2Int targetGridPos = currentGridPos + new Vector2Int((int)direction.x, (int)direction.y);

        if (levelManager.CanMove(currentGridPos, targetGridPos, direction))
        {
            // Hareket onayı alındı, LevelManager'ın verilerini güncelleyelim
            levelManager.MovePlayer(currentGridPos, targetGridPos, direction);
            targetPosition = levelManager.GridToWorld(targetGridPos);
            isMoving = true;
        }
    }

    void MoveSmoothly()
    {
        if (Vector2.Distance(transform.position, targetPosition) > 0.01f)
        {
            transform.position = Vector2.MoveTowards(transform.position, targetPosition, moveSpeed * Time.deltaTime);
        }
        else if (isMoving)
        {
            transform.position = targetPosition;
            isMoving = false;
        }
    }
}

Kutu İtme Mantığı: Adım Adım Uygulama

Oyunun çekirdeği olan Sokoban mekanikleri, oyuncunun bir sonraki hücreye hareket etme isteğiyle başlar. Bu istek, hedeflenen hücredeki nesnenin türüne göre farklı sonuçlar doğurur:

  1. Hedef Hücre Boşsa: Oyuncu doğrudan o hücreye hareket edebilir. Grid üzerindeki oyuncunun konumu güncellenir.
  2. Hedef Hücrede Duvar Varsa: Oyuncu hareket edemez. Duvarlar geçilemez engellerdir.
  3. Hedef Hücrede Kutu Varsa: Bu en kritik senaryodur. Oyuncu kutuyu itmeye çalışır. Bunun için, kutunun itileceği bir sonraki hücrenin (oyuncunun hareket yönünde, kutudan sonraki hücre) durumunu kontrol etmek gerekir:
    • Kutunun İtileceği Hücre Boşsa: Hem kutu hem de oyuncu hareket edebilir. Kutu hedef hücreye, oyuncu ise kutunun eski yerine geçer. Grid üzerindeki hem oyuncunun hem de kutunun konumu güncellenir.
    • Kutunun İtileceği Hücrede Duvar Varsa: Kutu hareket edemez, dolayısıyla oyuncu da kutuyu itemez ve hareket edemez.
    • Kutunun İtileceği Hücrede Başka Bir Kutu Varsa: Yine, kutu hareket edemez (çünkü aynı anda birden fazla kutu itemeyiz), dolayısıyla oyuncu da hareket edemez.

Bu mantığı yönetmek için, genellikle bir LevelManager veya GridManager sınıfı kullanılır. Bu sınıf, tüm grid verilerini (duvarlar, kutular, oyuncu konumu, hedef noktaları) saklar ve hareket isteklerini işler.

public class LevelManager : MonoBehaviour
{
    public GameObject playerPrefab, boxPrefab, wallPrefab, targetPrefab;
    public Transform gridParent; // Tüm grid objelerinin parent'ı
    public int gridSizeX, gridSizeY;
    private char[,] grid; // 'W', 'P', 'B', 'T', ' '
    private GameObject[,] gridObjects; // Gerçek GameObject'leri tutar

    // Örnek bir seviye haritası
    private string[] levelMap = new string[]
    {
        "WWWWW",
        "W P W",
        "W B W",
        "W T W",
        "WWWWW"
    };

    void Awake()
    {
        gridSizeX = levelMap[0].Length;
        gridSizeY = levelMap.Length;
        grid = new char[gridSizeX, gridSizeY];
        gridObjects = new GameObject[gridSizeX, gridSizeY];
        LoadLevel(levelMap);
    }

    void LoadLevel(string[] map)
    {
        for (int y = 0; y < gridSizeY; y++)
        {
            for (int x = 0; x < gridSizeX; x++)
            {
                grid[x, y] = map[y][x];
                Vector3 worldPos = GridToWorld(new Vector2Int(x, y));
                GameObject obj = null;

                switch (grid[x, y])
                {
                    case 'W': obj = Instantiate(wallPrefab, worldPos, Quaternion.identity, gridParent); break;
                    case 'P': obj = Instantiate(playerPrefab, worldPos, Quaternion.identity, gridParent); break;
                    case 'B': obj = Instantiate(boxPrefab, worldPos, Quaternion.identity, gridParent); break;
                    case 'T': obj = Instantiate(targetPrefab, worldPos, Quaternion.identity, gridParent); break;
                    // ' ' için boş bırak
                }
                gridObjects[x, y] = obj;
            }
        }
    }

    public Vector2Int WorldToGrid(Vector3 worldPos)
    {
        // World position'ı grid koordinatlarına çevirir
        // Örn: (0,0) merkezli ise (int)worldPos.x, (int)worldPos.y
        return new Vector2Int(Mathf.RoundToInt(worldPos.x), Mathf.RoundToInt(worldPos.y));
    }

    public Vector3 GridToWorld(Vector2Int gridPos)
    {
        // Grid koordinatlarını world position'a çevirir
        return new Vector3(gridPos.x, gridPos.y, 0);
    }

    public bool IsValidGridPos(Vector2Int pos)
    {
        return pos.x >= 0 && pos.x < gridSizeX && pos.y >= 0 && pos.y < gridSizeY;
    }

    public char GetCellContent(Vector2Int pos)
    {
        if (!IsValidGridPos(pos)) return 'W'; // Sınır dışıysa duvar gibi davran
        return grid[pos.x, pos.y];
    }

    public bool CanMove(Vector2Int playerCurrentPos, Vector2Int playerTargetPos, Vector2Int direction)
    {
        if (!IsValidGridPos(playerTargetPos)) return false; // Duvara çarpmış gibi

        char targetCellContent = GetCellContent(playerTargetPos);

        if (targetCellContent == 'W')
        {
            return false; // Duvar var, hareket edemezsin
        }
        else if (targetCellContent == 'B')
        {
            // Hedef hücrede kutu var, kutuyu itmeye çalış
            Vector2Int boxTargetPos = playerTargetPos + direction;
            if (!IsValidGridPos(boxTargetPos)) return false; // Kutu sınır dışına itilemez
            char boxTargetCellContent = GetCellContent(boxTargetPos);

            if (boxTargetCellContent == 'W' || boxTargetCellContent == 'B')
            {
                return false; // Kutunun gideceği yerde duvar veya başka kutu var
            }
            // Kutu itilebilir, o zaman oyuncu da hareket edebilir
            return true;
        }
        // Hedef hücre boş veya hedef (T)
        return true;
    }

    public void MovePlayer(Vector2Int playerCurrentPos, Vector2Int playerTargetPos, Vector2Int direction)
    {
        char targetCellContent = GetCellContent(playerTargetPos);

        if (targetCellContent == 'B')
        {
            // Kutu itiliyor
            Vector2Int boxTargetPos = playerTargetPos + direction;

            // Grid verilerini güncelle
            grid[boxTargetPos.x, boxTargetPos.y] = 'B'; // Kutunun yeni yeri
            grid[playerTargetPos.x, playerTargetPos.y] = ' '; // Kutunun eski yeri boşaldı

            // GameObject'leri güncelle
            GameObject boxToMove = gridObjects[playerTargetPos.x, playerTargetPos.y];
            boxToMove.transform.position = GridToWorld(boxTargetPos); // Kutuyu yeni konumuna taşı
            gridObjects[boxTargetPos.x, boxTargetPos.y] = boxToMove; // gridObjects'i güncelle
            gridObjects[playerTargetPos.x, playerTargetPos.y] = null; // Eski kutu konumunu boşalt
        }

        // Oyuncu hareket ediyor
        grid[playerTargetPos.x, playerTargetPos.y] = 'P'; // Oyuncunun yeni yeri
        grid[playerCurrentPos.x, playerCurrentPos.y] = ' '; // Oyuncunun eski yeri boşaldı (eğer hedef değilse)

        // Player GameObject'i PlayerMovement script'i tarafından taşınacak
        // gridObjects'te oyuncuyu güncellemeye gerek yok, PlayerMovement kendi transform'unu yönetiyor.
        // Ancak LevelManager'ın player referansını güncel tutmak iyi olabilir.
    }
}

Pratik İpuçları ile Sokoban Mekaniklerini Geliştirme

Temel Sokoban mekanikleri anlaşıldıktan sonra, oyun deneyimini zenginleştirecek bazı pratik ipuçlarına göz atalım:

  • Vector2Int Kullanımı: Grid tabanlı oyunlarda `Vector2Int` kullanmak, `Vector2`'ye göre daha mantıklıdır. `Vector2` kayan noktalı sayılarla çalıştığı için hassasiyet sorunlarına yol açabilirken, `Vector2Int` tam sayılarla grid koordinatlarını doğrudan temsil eder, bu da kodunuzu daha temiz ve hatasız hale getirir.
  • Akıcı Hareket Animasyonları: Oyuncu ve kutuların anında bir hücreden diğerine zıplaması yerine, `Vector2.Lerp` veya `Vector2.MoveTowards` kullanarak akıcı hareket animasyonları ekleyebilirsiniz. Bu, oyuna daha profesyonel ve tatmin edici bir his katar. Yukarıdaki örnek kodda MoveSmoothly metodu bunun için bir başlangıç noktasıdır.
  • Geri Alma (Undo) Sistemi: Sokoban oyunlarının vazgeçilmez özelliklerinden biri 'geri alma' yeteneğidir. Her hareket sonrası oyunun durumunu (oyuncu konumu, kutu konumları) bir `List` içinde saklayarak basit bir geri alma sistemi uygulayabilirsiniz. Örneğin, her hamlede `PlayerState` veya `GameState` adında bir sınıfın bir örneğini kaydedebilirsiniz.
  • Hedef Takibi ve Seviye Tamamlama: Oyuncunun tüm kutuları hedef noktalarına itip itmediğini kontrol etmek için, `LevelManager`'da hedef noktalarını ('T') ve o anki kutu konumlarını karşılaştıran bir metot (CheckWinCondition()) bulundurun. Kutular hedeflere ulaştığında görsel bir geri bildirim (örneğin, hedefin rengini değiştirmek) ekleyebilirsiniz.

Yaygın Hatalar ve Çözümleri

Sokoban mekanikleri geliştirirken karşılaşılan bazı yaygın hatalar ve bunların çözümleri şunlardır:

  • Sınır Dışına Çıkma: Oyuncunun veya kutuların grid sınırlarının dışına çıkmasını engellemek için her hareket kontrolünde `IsValidGridPos()` gibi bir metot kullanarak sınır kontrolleri yapın. Eğer hedef konum geçersizse, hareketin gerçekleşmesini engelleyin.
  • Yanlış Çarpışma Algılamaları: Özellikle iki kutu üst üste veya kutu-duvar senaryolarında mantık hataları sıkça görülür. `CanMove()` metodunuzu dikkatlice test edin ve her olası senaryoyu (boş, duvar, kutu, hedef) ayrı ayrı ele aldığınızdan emin olun. Debugging için `Debug.Log()` kullanarak hücre içeriklerini ve hedef konumları kontrol etmek çok faydalıdır.
  • Fizik Motoru ile Sorunlar: Unity'nin fizik motorunu (Rigidbody2D, Collider2D) grid tabanlı hareket için kullanmaya çalışmak genellikle gereksiz karmaşıklık ve performans sorunlarına yol açar. Sokoban gibi oyunlar için, yukarıda gösterildiği gibi manuel grid tabanlı mantık çok daha verimli ve öngörülebilirdir. Fizik motoru yerine kendi `CanMove` ve `MovePlayer` mantığınızı kullanın.

Performans ve Optimizasyon Notları

Grid tabanlı oyunlarda performans genellikle büyük bir sorun teşkil etmez, ancak yine de dikkat edilmesi gereken bazı noktalar vardır:

  • Fizik Motorundan Kaçının: Daha önce de belirtildiği gibi, `Rigidbody2D` ve `Collider2D` gibi Unity fizik bileşenlerini sadece görsel efektler veya çok özel durumlar için kullanın. Temel hareket ve çarpışma mantığını C# kodunuzla yönetmek, yüzlerce fizik objesini yönetmekten çok daha hafiftir.
  • Gereksiz Bellek Tahsisinden Kaçının: Özellikle `Update` döngüsünde sürekli yeni `Vector2` veya `Vector2Int` nesneleri oluşturmak yerine, mevcut nesneleri yeniden kullanmaya çalışın. Küçük projelerde bu çok fark etmese de, büyük ve karmaşık oyunlarda önemlidir.
  • Veri Yapısı Seçimi: `char[,]` veya `int[,]` gibi diziler, grid verilerini saklamak için hızlı ve bellek açısından verimli seçeneklerdir. `Dictionary` gibi yapılar daha esnek olabilir ancak erişim hızları dizilere göre biraz daha yavaş olabilir. Projenizin ihtiyaçlarına göre doğru veri yapısını seçin.

Sonuç

Unity ve C# kullanarak basit ama işlevsel Sokoban mekanikleri oluşturmak, grid tabanlı oyun geliştirmenin temel taşlarından biridir. Bu makalede ele aldığımız konular — grid sistemi, oyuncu ve kutu hareket mantığı, pratik ipuçları ve yaygın hataların çözümleri — kendi bulmaca oyunlarınızı geliştirmeniz için sağlam bir temel oluşturacaktır. Unutmayın, oyun geliştirme sürekli bir öğrenme sürecidir. Bu temel mekaniklerin üzerine kendi yaratıcı fikirlerinizi ekleyerek benzersiz Sokoban deneyimleri yaratabilirsiniz. Şimdi sıra sizde, Unity'yi açın ve kutuları itmeye başlayın!

🧠 Ders Sonu Değerlendirme Testi

Dersi tamamladıktan sonra bilgilerinizi test edin ve ekstra puanlar kazanın.

🔥 +50 XP Ödül
🔒

Sınava Katılmak İçin Giriş Yapın

Bu ders sonu testini çözebilmek, bilginizi test edip **+50 XP** kazanmak ve **Sınav Şampiyonu** rozetinin kilidini açmak için üye girişi yapmalısınız.

Yorumlar (0)

Yorum yazabilmek ve derslere katkıda bulunabilmek için giriş yapmalısınız.

İlk yorumu siz yapın!