Takip et

Unity’de Oyun Verisi Kaydetme: ScriptableObjects ile Güçlü Çözümler

Oyun geliştirme sürecinde, oyuncuların ilerlemesini kaydetmek ve bir sonraki oturumda kaldıkları yerden devam etmelerini sağlamak kritik bir öneme sahiptir. Peki, bu verileri nasıl daha düzenli, daha esnek ve daha performanslı bir şekilde yönetebiliriz? Unity’de oyun verilerini kaydetmenin çeşitli yolları bulunmaktadır; önceki yazılarımızda PlayerPrefs ve JSON/Binary serileştirmesi gibi yöntemleri ele almıştık. Bu makalemizde ise, veri yönetimini tamamen yeni bir seviyeye taşıyan, tasarım zamanı verilerini düzenlemede eşsiz bir araç olan ScriptableObjects’ı detaylı bir şekilde inceleyeceğiz. Bu güçlü Unity özelliği ile oyun verilerinizi kalıcı ve esnek bir şekilde nasıl saklayacağınızı, tasarımda kolaylık ve performansta artış için adım adım rehberimizi takip ederek öğrenin.

Her oyunun temelinde bir veri yönetimi yatar. Oyuncu ilerlemesi, envanter, karakter istatistikleri, görev durumu ve hatta oyun içi ayarlar gibi sayısız bilgi, oyuncu deneyiminin ayrılmaz bir parçasıdır. Bu verilerin güvenli, tutarlı ve erişilebilir bir şekilde kaydedilmesi, bir oyunun başarısı için hayati önem taşır. Aksi takdirde, oyuncuların emeklerinin boşa gitmesi, hayal kırıklığına ve nihayetinde oyunu bırakmalarına yol açabilir. PlayerPrefs gibi basit çözümler küçük ölçekli veriler için uygun olsa da, daha karmaşık yapıdaki oyunlar için yetersiz kalır. JSON veya Binary serileştirmesi, daha geniş veri kümelerini yönetme konusunda daha fazla esneklik sunar ancak çoğu zaman verilerin çalışma zamanında (runtime) oluşturulup kaydedilmesini gerektirir.

İşte tam da bu noktada ScriptableObjects devreye giriyor ve oyun geliştiricilere veri yönetimi konusunda devrim niteliğinde bir yaklaşım sunuyor. ScriptableObjects, temel olarak disk üzerinde bir “Asset” olarak var olan, MonoBehaviour’dan türemeyen, ancak Unity Editör’ünde oluşturulabilen ve yönetilebilen bir veri kapsayıcısıdır. Bu, onların çalışma zamanı (runtime) bileşenlerinden bağımsız olduğu anlamına gelir; yani bir Scene’e veya GameObject’e bağlı olmak zorunda değillerdir. Bu bağımsızlık, verilerin yeniden kullanılabilirliğini, düzenlenmesini ve farklı oyun objeleri arasında paylaşılmasını inanılmaz derecede kolaylaştırır. Örneğin, bir düşmanın temel istatistiklerini içeren bir ScriptableObject oluşturabilir ve bu aynı ScriptableObject asset’ini onlarca farklı düşman prefab’ında kullanabilirsiniz. Bu sayede, tek bir merkezi yerden tüm düşmanların belirli bir özelliğini güncelleyebilir, hata yapma riskini azaltabilir ve geliştirme sürecini hızlandırabilirsiniz.

Geleneksel yaklaşımlarda, veri çoğu zaman doğrudan GameObject’ler üzerindeki bileşenlere gömülür. Bu durum, verilerin tekrarını artırır, düzenlemeyi zorlaştırır ve projenin ölçeklenebilirliğini sınırlar. ScriptableObjects ise, verileri mantıksal olarak ayrıştırma ve merkezi bir konumda toplama imkanı sunar. Bu, özellikle büyük ve karmaşık projelerde, oyunun veri mimarisini temiz, anlaşılır ve bakımı kolay hale getirir. Oyuncu profilleri, envanter öğeleri, yetenek ağaçları, düşman tipleri, seviye konfigürasyonları gibi pek çok farklı veri türü ScriptableObjects aracılığıyla çok daha etkili bir şekilde yönetilebilir. Bu yaklaşım, sadece geliştirme verimliliğini artırmakla kalmaz, aynı zamanda oyunun performansına da olumlu katkıda bulunur, çünkü bellekte aynı verinin birden çok kopyası yerine tek bir referans tutulur. Bu da, özellikle mobil platformlar gibi bellek kısıtlı ortamlarda fark yaratabilir.

ScriptableObjects Nedir ve Veri Yönetimine Nasıl Yeni Bir Bakış Açar?

ScriptableObjects, Unity’nin en az anlaşılan ama en güçlü özelliklerinden biridir. Temel olarak, bunlar UnityEngine.ScriptableObject sınıfından türeyen ve proje içerisinde bir varlık (Asset) olarak diskte kaydedilebilen C# sınıflarıdır. En belirgin farkları, MonoBehaviour‘dan türememeleridir; yani bir sahneye bir bileşen olarak eklenemezler ve bir GameObject’in yaşam döngüsüne (Awake, Start, Update vb.) doğrudan katılmazlar. Peki, bu onları veri yönetimi için neden bu kadar ideal kılar?

Birincisi, bağımsızlık. ScriptableObjects, sahne veya GameObject hiyerarşisinden tamamen bağımsızdır. Bu, onları oyununuzdaki farklı nesneler arasında veya hatta farklı sahneler arasında paylaşılabilir veri kapsayıcıları haline getirir. Örneğin, bir RPG oyununda tüm eşyaların temel özelliklerini (adı, açıklaması, simgesi, değeri) içeren bir ItemData ScriptableObject’i tanımlayabilirsiniz. Her bir eşya (kılıç, zırh, iksir) bu ItemData‘nın ayrı bir örneği olacaktır, ancak tümü aynı yapıya sahip olacaktır. Bu yapı, veri tekrarını önler ve tutarlılığı sağlar.

İkincisi, Editör Entegrasyonu. [CreateAssetMenu] niteliğini kullanarak, Unity Editör’ünde sağ tıklayarak yeni bir ScriptableObject asset’i oluşturabilirsiniz. Bu, oyun tasarımcılarının veya seviye tasarımcılarının doğrudan Editör üzerinden veri girmelerini ve düzenlemelerini sağlar, kod yazmaya gerek kalmadan oyunun içeriğini zenginleştirmelerine olanak tanır. Verilerin koddan ayrılması, geliştirme süreçlerini birbirinden bağımsız hale getirir ve paralel çalışmayı kolaylaştırır.

Üçüncüsü, yeniden kullanılabilirlik. Bir ScriptableObject asset’i bir kez oluşturulduğunda, projenin her yerinde referans olarak kullanılabilir. Bu, aynı düşman tipi için aynı istatistikleri, aynı mermi tipi için aynı özellikleri veya aynı ses efekti ayarlarını birden fazla yerde kullanmak istediğinizde çok değerlidir. Bellekte yalnızca tek bir ScriptableObject asset’inin bir kopyası bulunur ve tüm referanslar bu tek kopyayı işaret eder. Bu durum, bellek kullanımını önemli ölçüde optimize eder ve çalışma zamanı performansını artırır. Geleneksel C# sınıfları, bir GameObject üzerinde bir bileşen olarak kullanıldığında veya bir değer tipi olarak kopyalandığında, her bir örnek için bellek tahsisi gerektirir. ScriptableObjects ise referans tipi olarak çalıştığı ve bir Asset olduğu için bu tür tekrar eden bellek tahsislerinden kaçınmanızı sağlar.

Son olarak, verinin ayrıştırılması ve modülerlik. Oyun mantığını ve veriyi birbirinden ayırmak, daha temiz, daha sürdürülebilir ve daha esnek bir kod tabanı oluşturmanın anahtarıdır. ScriptableObjects bu ayrımı doğal bir şekilde destekler. Oyunun davranışsal kısmı (örneğin, bir düşmanın nasıl hareket ettiği veya saldırdığı) MonoBehaviour‘lar tarafından yönetilirken, düşmanın temel istatistikleri (can, hasar, hızı) bir ScriptableObject tarafından sağlanabilir. Bu modüler yapı, kodun okunabilirliğini artırır, hata ayıklamayı kolaylaştırır ve gelecekteki değişiklikleri veya genişletmeleri basitleştirir. Böylece, bir düşman davranışını değiştirmek istediğinizde, veri yapısına dokunmanıza gerek kalmaz; aynı şekilde, düşmanın istatistiklerini değiştirmek istediğinizde, davranış kodunu güncellemek zorunda kalmazsınız. Bu güçlü ayrım, büyük ölçekli oyun projelerinde geliştirme ekipleri arasındaki işbirliğini de önemli ölçüde kolaylaştırır.

ScriptableObject Temel Kullanımı: İlk ScriptableObject’imizi Nasıl Oluştururuz?

Şimdi, ScriptableObjects’ın temellerini pratik bir örnekle pekiştirelim. Bir oyuncu profili oluşturmak ve bu profilin temel bilgilerini (oyuncu adı, seviye, deneyim puanı) ScriptableObject kullanarak nasıl saklayacağımızı görelim.

Adım 1: ScriptableObject Sınıfını Oluşturma
Öncelikle, yeni bir C# betiği oluşturun ve adını “PlayerProfileSO” koyun. İçeriğini aşağıdaki gibi düzenleyin:


using UnityEngine;

[CreateAssetMenu(fileName = "YeniOyuncuProfili", menuName = "Oyun Verileri/Oyuncu Profili")]
public class PlayerProfileSO : ScriptableObject
{
    public string playerName = "Yeni Oyuncu";
    public int level = 1;
    public long experiencePoints = 0;
    public int health = 100;
    public int mana = 50;

    public void GainExperience(int amount)
    {
        experiencePoints += amount;
        Debug.Log($"Oyuncu {playerName} {amount} deneyim kazandı. Toplam: {experiencePoints}");
        CheckLevelUp();
    }

    private void CheckLevelUp()
    {
        // Basit bir seviye atlama mantığı
        if (experiencePoints >= level * 100)
        {
            level++;
            experiencePoints = 0; // Yeni seviyede deneyimi sıfırla
            Debug.Log($"Oyuncu {playerName} seviye atladı! Yeni seviye: {level}");
        }
    }

    public void TakeDamage(int damageAmount)
    {
        health -= damageAmount;
        Debug.Log($"Oyuncu {playerName} {damageAmount} hasar aldı. Kalan Can: {health}");
        if (health <= 0)
        {
            Debug.Log($"{playerName} yenildi!");
        }
    }

    public void RestoreHealth(int amount)
    {
        health += amount;
        if (health > 100) health = 100; // Maksimum canı aşma
        Debug.Log($"{playerName} {amount} can yeniledi. Güncel Can: {health}");
    }
}

Buradaki kilit nokta [CreateAssetMenu] niteliğidir. Bu nitelik sayesinde, Unity Editör'ünde proje penceresinde sağ tıklayarak "Create" -> "Oyun Verileri" -> "Oyuncu Profili" yoluyla bu ScriptableObject'ten bir asset oluşturabilirsiniz. fileName varsayılan dosya adını, menuName ise menüdeki hiyerarşiyi belirler.

Adım 2: ScriptableObject Asset'ini Oluşturma
Unity Editör'üne dönün. Proje pencerenizde, uygun bir klasörde (örneğin "Assets/ScriptableObjects") sağ tıklayın, "Create" -> "Oyun Verileri" -> "Oyuncu Profili" seçeneğini tıklayın. Yeni oluşturulan asset'e "MainPlayerProfile" gibi bir ad verin. Inspector penceresinde, bu asset'in değişkenlerini (playerName, level, experiencePoints vb.) doğrudan düzenleyebilirsiniz.

Adım 3: ScriptableObject Verilerine Bir MonoBehaviour'dan Erişme
Şimdi, oyun içinde bu oyuncu profili verilerine nasıl erişeceğimizi görelim. Yeni bir C# betiği oluşturun, adını "GameManager" koyun ve bir GameObject'e ekleyin (örneğin boş bir GameObject oluşturup ona ekleyin). İçeriğini aşağıdaki gibi düzenleyin:


using UnityEngine;

public class GameManager : MonoBehaviour
{
    [SerializeField] private PlayerProfileSO currentPlayerProfile;

    void Start()
    {
        if (currentPlayerProfile != null)
        {
            Debug.Log($"Oyun Başladı! Oyuncu Adı: {currentPlayerProfile.playerName}, Seviye: {currentPlayerProfile.level}");
            
            // Oyuncuya deneyim kazandıralım
            currentPlayerProfile.GainExperience(50);
            currentPlayerProfile.GainExperience(80);
            currentPlayerProfile.TakeDamage(20);
            currentPlayerProfile.RestoreHealth(10);
        }
        else
        {
            Debug.LogError("Oyuncu Profili ScriptableObject atanmadı!");
        }
    }

    // Oyun durduğunda profilin anlık durumunu kaydetmek için (runtime değişiklikleri asset'e kaydolmaz!)
    // Bu kısım sonraki bölümde detaylandırılacak.
    void OnApplicationQuit()
    {
        if (currentPlayerProfile != null)
        {
            Debug.Log($"Oyun Kapanıyor. Son Seviye: {currentPlayerProfile.level}, Son Deneyim: {currentPlayerProfile.experiencePoints}");
            // Normalde burada bir kaydetme fonksiyonu çağırırdık
        }
    }
}

Unity Editör'üne geri dönün, GameManager bileşeninin Inspector penceresinde "Current Player Profile" alanını göreceksiniz. Buraya, daha önce oluşturduğunuz "MainPlayerProfile" asset'ini sürükleyip bırakın. Oyunu çalıştırdığınızda, Debug.Log mesajlarını konsolda göreceksiniz. PlayerProfileSO üzerindeki metodları çağırarak verileri güncellediğinizi fark edeceksiniz. Ancak dikkat: Oyunu durdurduğunuzda, Inspector penceresindeki "MainPlayerProfile" asset'inin verilerinin orijinal haline geri döndüğünü göreceksiniz (level 1, experience 0). Bunun nedeni, ScriptableObjects'ın çalışma zamanında yapılan değişiklikleri otomatik olarak Asset dosyasına kaydetmemesidir. Bu, bir sonraki bölümümüzün ana konusudur: veri kalıcılığı.

Uzman İpucu: ScriptableObjects sadece tekil veri setleri için değil, aynı zamanda veri listeleri veya dizileri için de mükemmeldir. Örneğin, bir EnemyWaveDataSO oluşturup içine bir List tutarak farklı düşman dalgalarını tanımlayabilir ve bunları Editör üzerinden kolayca ayarlayabilirsiniz.

ScriptableObjects ile Veri Kalıcılığı Nasıl Sağlanır?

Önceki bölümde de belirtildiği gibi, ScriptableObjects'a çalışma zamanında (runtime) yapılan değişiklikler, Unity Editör'ü durdurulduğunda veya oyun kapatıldığında kaybolur. Bunun nedeni, ScriptableObjects'ın temel olarak tasarım zamanı verilerini depolamak için tasarlanmış olmalarıdır; bir "Asset" olarak diskte saklanırlar ve çalışma zamanındaki değişiklikler geçicidir. Bu durum, oyuncunun ilerlemesini veya oyunun dinamik durumunu kaydetmek istediğimizde bir zorluk teşkil eder. Peki, bu sorunu nasıl aşabiliriz ve ScriptableObjects ile kalıcı veri depolama yeteneğini nasıl birleştirebiliriz?

Çözüm, ScriptableObjects'ı harici bir serileştirme mekanizmasıyla birlikte kullanmaktır. Yani, oyun çalışırken ScriptableObject'imizdeki verileri güncelleyeceğiz, ancak oyunu kapatmadan önce bu verilerin anlık durumunu harici bir dosyaya kaydedeceğiz (JSON, Binary veya başka bir format kullanarak). Oyun başladığında ise, bu harici dosyadan verileri okuyup ScriptableObject'imizin içeriğini güncelleyeceğiz. Bu yöntem, ScriptableObjects'ın sağladığı kolay veri yönetimi ve Editör entegrasyonu avantajlarından vazgeçmeden, aynı zamanda veri kalıcılığı ihtiyacımızı da karşılamamızı sağlar. Genellikle JsonUtility, basitliği ve platformlar arası uyumluluğu nedeniyle bu tür senaryolarda tercih edilen bir yöntemdir.

Vaka Analizi: Envanter Sistemi Veri Kaydı

Bir RPG oyununda oyuncunun envanterini düşünelim. Her eşya bir ScriptableObject (InventoryItemSO) olarak tanımlanabilir, ancak oyuncunun envanterindeki belirli bir eşyanın sayısı veya durumu çalışma zamanında değişebilir. Bu dinamik verileri nasıl kaydedip yükleyeceğiz?

Adım 1: Envanter Öğesi ScriptableObject'ini Tanımlama
Önce her bir envanter öğesinin temel özelliklerini tutacak bir ScriptableObject oluşturalım:


using UnityEngine;

[CreateAssetMenu(fileName = "YeniEnvanterEsyasi", menuName = "Oyun Verileri/Envanter Esyasi")]
public class InventoryItemSO : ScriptableObject
{
    public string itemName = "Yeni Eşya";
    public Sprite icon; // Eşya ikonu
    [TextArea(3, 5)]
    public string description = "Bu bir eşyadır.";
    public int value = 1; // Eşyanın değeri
    public bool isStackable = true; // İstiflenebilir mi?
    public int maxStackSize = 99; // Maksimum istif boyutu

    // Benzersiz bir ID ataması (isteğe bağlı ama faydalı)
    public string itemID; 

    void OnEnable()
    {
        if (string.IsNullOrEmpty(itemID))
        {
            itemID = System.Guid.NewGuid().ToString(); // Otomatik benzersiz ID ata
        }
    }
}

Bu ScriptableObject'ten birkaç tane oluşturun (örneğin "Sword", "HealthPotion").

Adım 2: Kaydedilebilir Veri Yapısı Oluşturma
Şimdi, oyuncunun envanterindeki eşyaların anlık durumunu (hangi eşya, kaç tane) kaydedecek bir POCO (Plain Old C# Object) sınıfı tanımlayalım. Bu sınıf, JsonUtility tarafından serileştirilebilecek şekilde tasarlanmalıdır.


using System.Collections.Generic;
using UnityEngine; // Sadece Debug için

[System.Serializable]
public class InventorySlotData
{
    public string itemID; // Hangi InventoryItemSO'ya ait olduğunu belirten ID
    public int quantity; // Bu eşyadan kaç tane var

    public InventorySlotData(string id, int qty)
    {
        itemID = id;
        quantity = qty;
    }
}

[System.Serializable]
public class InventorySaveData
{
    public List slots = new List();

    // Kolay erişim için (örneğin, tüm ScriptableObject'leri bir sözlükte tutarak)
    // Bu kısım genelde runtime'da doldurulur, serileştirilmez.
    [System.NonSerialized] 
    public Dictionary itemDatabase; 
}

Adım 3: Envanter Yöneticisi ve Kaydetme/Yükleme Mantığı
Son olarak, bu verileri yönetecek ve kaydedip yükleyecek bir InventoryManager sınıfı oluşturalım. Bu sınıf, InventorySaveData nesnesini kullanarak verileri diskten okuyacak ve diske yazacaktır.


using UnityEngine;
using System.IO;
using System.Collections.Generic;
using System.Linq; // LINQ için

public class InventoryManager : MonoBehaviour
{
    public List allAvailableItems; // Editörden atanacak tüm olası eşyalar
    private InventorySaveData currentInventoryData;
    private string savePath;

    // Çalışma zamanında envanterdeki eşyaların referanslarını tutmak için
    public Dictionary playerInventory = new Dictionary(); // itemID, quantity

    void Awake()
    {
        savePath = Path.Combine(Application.persistentDataPath, "inventory.json");
        currentInventoryData = new InventorySaveData();

        // itemDatabase'i doldur (sadece bir kere yapılır)
        currentInventoryData.itemDatabase = new Dictionary();
        foreach (var item in allAvailableItems)
        {
            if (!string.IsNullOrEmpty(item.itemID))
            {
                currentInventoryData.itemDatabase[item.itemID] = item;
            }
        }

        LoadInventory();
    }

    public void AddItem(InventoryItemSO item, int quantity)
    {
        if (item == null || string.IsNullOrEmpty(item.itemID))
        {
            Debug.LogError("Geçersiz eşya veya eşya ID'si!");
            return;
        }

        if (playerInventory.ContainsKey(item.itemID))
        {
            playerInventory[item.itemID] += quantity;
        }
        else
        {
            playerInventory.Add(item.itemID, quantity);
        }
        Debug.Log($"{item.itemName} eklendi. Toplam: {playerInventory[item.itemID]}");
    }

    public void RemoveItem(InventoryItemSO item, int quantity)
    {
        if (item == null || string.IsNullOrEmpty(item.itemID))
        {
            Debug.LogError("Geçersiz eşya veya eşya ID'si!");
            return;
        }

        if (playerInventory.ContainsKey(item.itemID))
        {
            playerInventory[item.itemID] -= quantity;
            if (playerInventory[item.itemID] <= 0)
            {
                playerInventory.Remove(item.itemID);
                Debug.Log($"{item.itemName} envanterden kaldırıldı.");
            }
            else
            {
                Debug.Log($"{item.itemName} kaldırıldı. Kalan: {playerInventory[item.itemID]}");
            }
        }
    }

    public void SaveInventory()
    {
        currentInventoryData.slots.Clear();
        foreach (var entry in playerInventory)
        {
            currentInventoryData.slots.Add(new InventorySlotData(entry.Key, entry.Value));
        }

        string json = JsonUtility.ToJson(currentInventoryData, true); // true ile daha okunabilir format
        File.WriteAllText(savePath, json);
        Debug.Log("Envanter kaydedildi: " + savePath);
    }

    public void LoadInventory()
    {
        if (File.Exists(savePath))
        {
            string json = File.ReadAllText(savePath);
            InventorySaveData loadedData = JsonUtility.FromJson(json);
            
            // Yüklenen verileri playerInventory'e aktar
            playerInventory.Clear();
            foreach (var slot in loadedData.slots)
            {
                playerInventory.Add(slot.itemID, slot.quantity);
                // Eğer itemDatabase'imiz varsa, burada eşya ScriptableObject'ine referans da alabiliriz
                if (currentInventoryData.itemDatabase != null && currentInventoryData.itemDatabase.ContainsKey(slot.itemID))
                {
                    Debug.Log($"Yüklendi: {currentInventoryData.itemDatabase[slot.itemID].itemName} x {slot.quantity}");
                }
            }
            Debug.Log("Envanter yüklendi.");
        }
        else
        {
            Debug.Log("Kaydedilmiş envanter bulunamadı. Yeni envanter başlatılıyor.");
            // İlk kez oynayanlar için başlangıç eşyaları eklenebilir
            // AddItem(allAvailableItems[0], 1); // Örneğin ilk eşyadan bir tane ekle
        }
    }

    // Oyunu kapatırken envanteri kaydet
    void OnApplicationQuit()
    {
        SaveInventory();
    }

    // Test için
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.K))
        {
            SaveInventory();
        }
        if (Input.GetKeyDown(KeyCode.L))
        {
            LoadInventory();
        }
        if (Input.GetKeyDown(KeyCode.A)) // Eşya ekle
        {
            if (allAvailableItems.Count > 0)
            {
                AddItem(allAvailableItems[Random.Range(0, allAvailableItems.Count)], 1);
            }
        }
        if (Input.GetKeyDown(KeyCode.R)) // Rastgele eşya çıkar
        {
            if (playerInventory.Count > 0)
            {
                string randomItemID = playerInventory.Keys.ElementAt(Random.Range(0, playerInventory.Count));
                RemoveItem(currentInventoryData.itemDatabase[randomItemID], 1);
            }
        }
    }
}

Bu InventoryManager betiğini boş bir GameObject'e ekleyin. Inspector'da "All Available Items" listesine daha önce oluşturduğunuz InventoryItemSO asset'lerini sürükleyip bırakın. Oyunu çalıştırın, eşya ekleyip çıkarın (A ve R tuşlarıyla). Sonra K tuşuna basarak kaydedin. Oyunu kapatıp tekrar açtığınızda L tuşuna basmanıza gerek kalmadan Awake() içinde otomatik olarak yüklenmeli ve envanterinizin kaldığı yerden devam ettiğini göreceksiniz. OnApplicationQuit() sayesinde, oyun normal bir şekilde kapatıldığında envanteriniz otomatik olarak kaydedilecektir.

Performans ve Bellek Yönetimi Açısından ScriptableObjects'ın Avantajları Nelerdir?

ScriptableObjects'ın cazibesi sadece veri organizasyonu ve Editör entegrasyonuyla sınırlı değildir; aynı zamanda oyunlarınızın performansını ve bellek kullanımını önemli ölçüde optimize etme potansiyeline sahiptirler. Bu avantajlar, özellikle büyük ve karmaşık oyunlarda veya mobil gibi bellek kısıtlı platformlarda geliştirme yaparken hayati öneme sahip olabilir.

1. Bellek Tekrarının Önlenmesi: Geleneksel yaklaşımlarda, aynı türdeki birçok GameObject'in benzer verileri kendi bileşenlerinde saklaması yaygındır. Örneğin, farklı düşman GameObject'lerinin her biri kendi EnemyStats sınıfını tutabilir. Bu durumda, her bir düşman için ayrı bir EnemyStats nesnesi bellekte yer kaplar. Eğer oyununuzda yüzlerce düşman varsa, bu durum önemli bir bellek fazlalığına yol açabilir. ScriptableObjects bu sorunu ortadan kaldırır. Bir EnemyStatsSO oluşturur ve bu asset'i farklı düşman prefab'ları arasında paylaşırsınız. Bellekte yalnızca bir EnemyStatsSO örneği bulunur ve tüm düşmanlar bu tek örneğe referans verir. Bu, yüzlerce veya binlerce düşmanınız olduğunda bellek kullanımında dramatik bir düşüş sağlar.

2. Daha Az Serileştirme Yükü: Sahne yükleme ve boşaltma sırasında veya kaydetme/yükleme işlemleri esnasında verilerin serileştirilmesi ve deserializasyonu önemli bir zaman alabilir. Eğer verileriniz ScriptableObjects içinde merkezi olarak yönetiliyorsa, bu süreç daha verimli hale gelir. Özellikle oyununuzda çok sayıda GameObject varsa ve bu objeler kendi içlerinde büyük veri yapılarını barındırıyorsa, her bir GameObject'in serileştirilmesi uzun sürebilir. ScriptableObjects ile, tasarım zamanı verileri zaten asset olarak serileştirilmiş durumdadır ve çalışma zamanında sadece referanslar yüklenir. Runtime'da kaydedilen dynamic veriler için ise, yukarıda gördüğümüz gibi sadece değişen ve kaydedilmesi gereken verileri içeren minimal bir yapı serileştirilir, bu da işlemi hızlandırır.

3. Editor Performansı ve İş Akışı: Geliştirme sürecinde, Unity Editor'ü üzerinde çalışırken ScriptableObjects'ın faydaları daha da belirginleşir. Büyük miktarda veriyi bir kerede toplu olarak düzenleyebilir, farklı asset'ler arasında kolayca sürükle-bırak referansları oluşturabilirsiniz. Bu, oyun içeriğinin hızlı bir şekilde oluşturulmasına ve yinelemeli geliştirme döngülerinin hızlanmasına yardımcı olur. Verilerin koddan ayrı tutulması, kod derlemeleri sırasında değişiklik yapma ihtiyacını azaltır ve böylece Editor'deki "Compile" bekleme sürelerini kısaltabilir.

4. Önbellekleme ve Çalışma Zamanı Erişimi: ScriptableObjects, referans tipleri oldukları için, bellekte tek bir kopyaları bulunur ve bu kopyaya erişim çok hızlıdır. Bir GameObject üzerinde her zaman bir değer tipini kopyalamak veya her ihtiyaç duyulduğunda yeni bir nesne oluşturmak yerine, önceden yüklenmiş bir ScriptableObject asset'ine doğrudan erişebilirsiniz. Bu durum, özellikle sıkça erişilen ve paylaşılan veriler için mikro-optimizasyonlar sağlayarak genel oyun performansına katkıda bulunur. Karmaşık veri yapılarına sahip oyunlarda, veriye erişim sürelerinin minimize edilmesi, akıcı bir oyun deneyimi için kritik öneme sahiptir.

Uzman İpucu: Büyük ve statik veri kümeleri için (örneğin, bir dünya haritasındaki tüm konum noktaları, görev tanımları) ScriptableObjects kullanmak, verilerin Editör'den kolayca düzenlenmesini ve oyun başlatıldığında hızlıca yüklenmesini sağlar. Hatta bu ScriptableObjects'ları bir "Database" ScriptableObject'i içinde toplayarak merkezi bir erişim noktası oluşturabilirsiniz. Bu teknikle, başlangıç yükleme sürelerini optimize edebilir ve bellek kullanımını kontrol altında tutabilirsiniz.

İleri Düzey Kullanım Senaryoları ve En İyi Uygulamalar

ScriptableObjects'ın temel ve veri kaydetme mekanizmalarını öğrendikten sonra, şimdi daha karmaşık senaryolarda nasıl kullanılabileceğine ve en iyi uygulamalarına göz atalım. Bu bölüm, projelerinizde ScriptableObjects'ı daha verimli ve güçlü bir şekilde entegre etmenize yardımcı olacaktır.

Vaka Analizi: Karakter Yetenek Ağacı ve Gelişimi

Bir RPG oyununda karakterlerin yeteneklerini yönetmek genellikle karmaşık bir iştir. Her yeteneğin kendine özgü özellikleri, maliyetleri ve ön koşulları olabilir. Ayrıca, oyuncunun hangi yetenekleri açtığını da kaydetmemiz gerekir. ScriptableObjects bu senaryo için biçilmiş kaftandır.

1. Yetenek Tanımlamaları İçin SkillSO:
Her bir yeteneği (örneğin "Ateş Topu", "Hızlı Koşma") bir SkillSO ScriptableObject'i olarak tanımlarız.


using UnityEngine;
using System.Collections.Generic;

[CreateAssetMenu(fileName = "YeniYetenek", menuName = "Oyun Verileri/Yetenek")]
public class SkillSO : ScriptableObject
{
    public string skillName = "Yeni Yetenek";
    [TextArea(3, 5)]
    public string description = "Yetenek açıklaması.";
    public Sprite icon;
    public int manaCost = 10;
    public float cooldown = 5f;
    public List prerequisites; // Bu yeteneği açmak için gerekli yetenekler
    public string skillID; // Benzersiz ID

    void OnEnable()
    {
        if (string.IsNullOrEmpty(skillID))
        {
            skillID = System.Guid.NewGuid().ToString();
        }
    }
}

2. Yetenek Ağacı Yapısı İçin SkillTreeSO:
Tüm yetenekleri ve bunların birbiriyle ilişkilerini gösteren bir SkillTreeSO oluşturabiliriz. Bu, genellikle sadece bir liste veya ağaç yapısı içeren ve Editör üzerinden kolayca görselleştirilebilen bir ScriptableObject olabilir.


using UnityEngine;
using System.Collections.Generic;

[CreateAssetMenu(fileName = "YeniYetenekAgaci", menuName = "Oyun Verileri/Yetenek Agaci")]
public class SkillTreeSO : ScriptableObject
{
    public List allSkills; // Oyunun tüm yetenekleri
    // İstenirse, bu yeteneklerin birbiriyle olan bağlantılarını gösteren karmaşık bir yapı da eklenebilir.
}

3. Oyuncu İlerlemesini Kaydetme:
Oyuncunun hangi yetenekleri açtığını takip etmek için ayrı bir kaydetme sınıfı (örneğin PlayerSkillProgressData) oluştururuz. Bu sınıf, açılan yeteneklerin ID'lerini bir List olarak tutar ve JSON ile serileştirilir.


using System.Collections.Generic;

[System.Serializable]
public class PlayerSkillProgressData
{
    public List unlockedSkillIDs = new List();
}

SkillManager gibi bir MonoBehaviour, bu PlayerSkillProgressData'yı yükleyip kaydedecek ve SkillTreeSO'daki bilgilere göre yetenekleri yönetecektir. Oyuncu bir yetenek açtığında, o yeteneğin ID'sini unlockedSkillIDs listesine ekleriz ve oyunu kapatmadan önce bu listeyi kaydederiz. Oyun başladığında ise, listeyi yükleyip oyuncunun açtığı yetenekleri tekrar işaretleriz.

Runtime Değişiklikleri Yönetme (Sadece Editörde)

Yukarıdaki örneklerde, ScriptableObjects'a runtime'da yaptığımız değişikliklerin kalıcı olmadığını gördük. Ancak bazen, özellikle geliştirme ve hata ayıklama sırasında, ScriptableObject asset'lerinin değerlerini çalışma zamanında değiştirip bu değişiklikleri asset dosyasına kaydetmek isteyebiliriz. Bu, yalnızca Unity Editör'ünde mümkün olan bir durumdur ve UnityEditor.EditorUtility.SetDirty(this) ve UnityEditor.AssetDatabase.SaveAssets() gibi metodlar kullanılarak yapılır. Bu metodlar UnityEditor namespace'inde olduğu için, build'de derlenmeyecek şekilde #if UNITY_EDITOR makroları arasına alınmalıdır.


// PlayerProfileSO içinde bir örnek
public void SaveChangesToAsset()
{
#if UNITY_EDITOR
    UnityEditor.EditorUtility.SetDirty(this); // Bu asset'in değiştiğini işaretle
    UnityEditor.AssetDatabase.SaveAssets(); // Tüm kirli asset'leri kaydet
    Debug.Log($"{name} ScriptableObject asset'ine değişiklikler kaydedildi.");
#endif
}

Bu fonksiyonu çağırdığınızda, ScriptableObject'in güncel durumu doğrudan asset dosyasına kaydedilir ve Editor'ü kapatıp açsanız bile değişiklikler korunur. Bu, özellikle veri düzenleme ve test süreçlerinde çok faydalıdır.

Versiyonlama Stratejileri

Oyununuz geliştikçe, ScriptableObject yapılarınızın da değişmesi kaçınılmazdır. Yeni alanlar eklenebilir, mevcutlar kaldırılabilir veya veri tipleri değişebilir. Bu durumda, eski kaydedilmiş verilerin yeni veri şemasıyla uyumlu olmasını sağlamak önemlidir.

  • Basit Versiyonlama: Kaydedilen JSON dosyasına veya ScriptableObject'e bir version alanı ekleyin. Veri yüklenirken bu version numarasını kontrol edin. Eğer eski bir versiyon ise, verileri yeni şemaya dönüştürecek özel bir geçiş (migration) mantığı uygulayın.
  • Geriye Uyumluluk: Yeni alanlar eklerken, bunları [SerializeField] ile işaretleyin ve varsayılan değerler atayın. Eski veriler yüklenirken bu alanlar varsayılan değerlerini alacak ve hata oluşmayacaktır.
  • Aşırı Karmaşık Durumlarda: Eğer veri şemanız sık sık ve radikal bir şekilde değişiyorsa, daha gelişmiş serileştirme kütüphaneleri (örneğin Odin Inspector veya kendi custom serileştiricileriniz) veya harici bir veritabanı çözümü (SQLite) düşünmek gerekebilir.

Mobil Uyumlu HTML ve Oyun Arayüzleri: Makalenin mobil uyumluluk konusuna değinmek gerekirse, web tarafında olduğu gibi oyun arayüzlerinde de responsive tasarım ilkeleri uygulanabilir. ScriptableObjects doğrudan HTML üretmez, ancak oyun içi kullanıcı arayüzlerini (UI) tasarlarken, farklı ekran boyutları ve çözünürlükler için esnek UI panelleri ve düzenleri oluşturmak mümkündür. Unity'nin UI sistemi (Canvas, Rect Transform, Layout Groups), web'deki media query'lere benzer şekilde, ekran boyutuna göre öğelerin konumunu, boyutunu ve hatta görünürlüğünü ayarlayabilen araçlar sunar. Örneğin, bir envanter ekranının mobil cihazlarda daha kompakt, masaüstünde ise daha geniş bir görünüm sunması için Canvas Scaler ve Layout Groups ile dinamik ayarlamalar yapabilirsiniz. Bu, ScriptableObjects ile yönettiğiniz verilerin (örneğin envanterdeki eşyaların listesi) her platformda en iyi şekilde sunulmasını sağlar.

ScriptableObjects Kullanırken Karşılaşılabilecek Zorluklar ve Çözümleri

Her güçlü araç gibi, ScriptableObjects'ın da kendi zorlukları ve dikkate alınması gereken noktaları vardır. Bu bölümde, ScriptableObjects kullanırken karşılaşabileceğiniz yaygın sorunları ve bunlara yönelik çözümleri ele alacağız, böylece projelerinizde sorunsuz bir şekilde ilerleyebilirsiniz.

1. Runtime Değişikliklerinin Kalıcı Olmaması (En Büyük Zorluk):
* Zorluk: Daha önce de belirttiğimiz gibi, oyun çalışırken bir ScriptableObject'in değerlerini değiştirdiğinizde, bu değişiklikler Unity Editör'ü durdurulduğunda veya oyun kapatıldığında asset dosyasına kaydedilmez ve kaybolur. Bu, özellikle başlangıçta yanıltıcı olabilir ve oyuncu verilerini kaydetmek için ek bir mekanizma gerektirir.
* Çözüm: Oyuncu verileri gibi dinamik verileri ScriptableObjects'a kaydetmek istiyorsanız, yukarıdaki "Veri Kalıcılığı" bölümünde açıklandığı gibi harici bir serileştirme yöntemi (JSON, Binary) kullanmalısınız. Çalışma zamanında ScriptableObject'teki verileri güncelleyin, ancak oyun kapatılmadan önce bu verileri bir POCO sınıfına aktarıp diskteki bir dosyaya kaydedin. Oyun başladığında, diskteki dosyadan verileri okuyup ScriptableObject'inizin ilgili alanlarını güncelleyin. Yalnızca Editor'de geliştirme yaparken kalıcı değişikliklere ihtiyacınız varsa, EditorUtility.SetDirty() ve AssetDatabase.SaveAssets() kullanabilirsiniz, ancak bu kodları build'e dahil etmediğinizden emin olun (#if UNITY_EDITOR).

2. ScriptableObject Referanslarının Kaybı:
* Zorluk: Eğer bir GameObject veya başka bir ScriptableObject, bir ScriptableObject asset'ine referans veriyorsa ve siz bu asset'i proje penceresinde siler veya yerini değiştirirseniz, referans bozulabilir. Unity Editör'ü genellikle bu referansları otomatik olarak güncelleyebilse de, bazı durumlarda (özellikle doğrudan koddan AssetDatabase ile manuel işlem yapıldığında) sorunlar yaşanabilir.
* Çözüm: ScriptableObject asset'lerinizi proje içinde düzenli bir klasör yapısında tutun ve mümkün olduğunca yerlerini değiştirmekten kaçının. Eğer bir asset'i taşımak zorundaysanız, bunu Unity Editör'ünün proje penceresinden sürükleyip bırakarak yapın; böylece Unity referansları otomatik olarak güncelleyebilir. Asset'leri silmeden önce, "Find References In Scene" veya Inspector'daki sağ tıklama menüsünü kullanarak hangi diğer asset'lerin veya GameObject'lerin bu ScriptableObject'e referans verdiğini kontrol edin.

3. Büyük Veri Kümelerinin Yönetimi:
* Zorluk: ScriptableObjects, harika bir veri yönetim aracı olsa da, gigabaytlarca veri içeren devasa veri kümeleri için en uygun çözüm olmayabilir. Örneğin, açık dünya haritasındaki her bir ağacın konumunu veya her NPC'nin bireysel diyalog satırlarını binlerce ayrı ScriptableObject olarak tutmak, Editör'de yavaşlamalara veya disk boyutunda şişkinliğe yol açabilir.
* Çözüm: Büyük veri kümeleri için ScriptableObjects'ı soyutlamanın bir yolu olarak kullanın, ancak ayrıntılı verileri harici dosyalarda (CSV, JSON, XML) veya hafif bir veritabanında (SQLite) saklayın. ScriptableObject, bu harici veri kaynaklarına referansları veya yükleme mantığını tutabilir. Örneğin, bir WorldBiomeSO sadece bir biyomun genel özelliklerini tutarken, bu biyomdaki her ağacın konumu ayrı bir CSV dosyasında saklanabilir ve WorldBiomeSO bu dosyayı yüklemekten sorumlu olabilir. Bu yaklaşım, ScriptableObjects'ın kolay yönetim avantajlarından faydalanırken, büyük veri performans sorunlarını da çözmenize yardımcı olur.

4. Çok Oyunculu Ortamlarda Senkronizasyon:
* Zorluk: ScriptableObjects genellikle client-side veri yönetimi ve tek oyunculu oyunlar için harikadır. Ancak, çok oyunculu bir oyunda ScriptableObjects'taki runtime değişikliklerini tüm istemciler veya sunucu arasında senkronize etmek karmaşık bir süreçtir. ScriptableObjects network üzerinden otomatik olarak senkronize edilmez.
* Çözüm: Çok oyunculu ortamlarda, ScriptableObjects'ı genellikle "Salt Okunur" veri tanımlamaları için kullanın (örneğin eşya özellikleri, yetenek tanımlamaları). Oyuncunun envanteri, karakter istatistikleri gibi dinamik ve senkronize edilmesi gereken veriler için, oyununuzun ağ katmanına entegre edilmiş özel veri yapıları ve senkronizasyon mekanizmaları kullanın (örneğin, Mirror'da SyncVars, Photon'da Custom Properties). ScriptableObjects, bu ağ veri yapılarının kullandığı temel tanımlamaları sağlamaya devam edebilir, ancak actual state'i ağ katmanı yönetecek.

Bu zorlukları ve çözümleri bilmek, ScriptableObjects'ı projelerinizde daha bilinçli ve etkili bir şekilde kullanmanızı sağlayacaktır. Doğru senaryolarda doğru araçları kullanmak, her zaman başarılı bir oyun geliştirme sürecinin anahtarıdır.

Sonuç: ScriptableObjects ile Veri Yönetiminde Yeni Bir Dönem

Unity'de oyun verilerini kaydetme ve yönetme serimizin üçüncü bölümü olan bu makalede, ScriptableObjects'ın gücünü ve esnekliğini detaylı bir şekilde inceledik. PlayerPrefs'in basitliğinden ve JSON/Binary serileştirmesinin esnekliğinden sonra, ScriptableObjects'ın veri yönetimini tamamen farklı bir boyuta taşıdığını gördük. Bu özel asset türleri, verileri MonoBehaviour'lardan ayırarak, projenizde daha modüler, yeniden kullanılabilir ve sürdürülebilir bir yapı oluşturmanın kapılarını aralıyor.

ScriptableObjects sayesinde, oyun tasarımcıları ve geliştiriciler, kod tabanından bağımsız olarak oyun içeriğini daha kolay bir şekilde oluşturabilir ve düzenleyebilirler. Performans açısından, bellek tekrarını önlemesi ve veriye daha hızlı erişim sağlaması, özellikle büyük ölçekli ve bellek kısıtlı projelerde önemli avantajlar sunar. Envanter sistemleri, yetenek ağaçları, düşman tanımlamaları ve seviye konfigürasyonları gibi birçok karmaşık oyun sistemi için ScriptableObjects, veri mimarisi tasarımında vazgeçilmez bir araç haline gelmiştir.

Unutmamak gerekir ki, ScriptableObjects çalışma zamanı değişikliklerini otomatik olarak kaydetmez. Bu nedenle, oyuncu ilerlemesi gibi dinamik veriler için harici bir serileştirme mekanizması (genellikle JSON) ile birleştirilmeleri gerekir. Ancak bu kombinasyon, hem tasarım zamanı verilerinin kolay yönetimini hem de çalışma zamanı verilerinin kalıcılığını bir araya getiren güçlü bir hibrit çözüm sunar. Artık bu bilgilere sahip olduğunuza göre, kendi projelerinizde ScriptableObjects'ı denemekten çekinmeyin. Oyunlarınızın veri yönetimini optimize edin ve geliştirme sürecinizi bir üst seviyeye taşıyın!

Sıkça Sorulan Sorular

  • S: ScriptableObject'ler sadece Editörde mi kullanılır, yoksa build'de de çalışır mı?
    C: ScriptableObject'ler hem Unity Editör'ünde hem de oyunun build edilmiş halinde (runtime) çalışır. Asset olarak proje dosyalarınızın bir parçası olurlar ve oyun çalışırken belleğe yüklenerek içerdikleri verilere erişilebilir. Ancak, runtime'da yapılan değişiklikler build'de de asset dosyasına kalıcı olarak kaydedilmez; bunun için harici bir serileştirme mekanizması gerekir.
  • S: ScriptableObject'leri ne zaman kullanmalıyım, ne zaman normal C# sınıflarını tercih etmeliyim?
    C: ScriptableObject'leri, verilerinizi GameObjects veya Scene'lerden bağımsız olarak merkezi bir yerde tanımlamak, Editör üzerinden düzenlemek ve farklı bileşenler veya asset'ler arasında kolayca paylaşmak istediğinizde kullanmalısınız. Özellikle oyun varlıklarının (eşya tanımları, yetenekler, düşman tipleri) statik özelliklerini tutmak için idealdirler. Normal C# sınıflarını ise, genellikle anlık, geçici verileri (hesaplamalar, sadece bir method içinde kullanılan küçük veri yapıları) veya MonoBehaviour'lar üzerinde karmaşık bir davranışın parçası olarak kullanmak istediğinizde tercih edebilirsiniz. Eğer veri sürekli değişecek ve oyun durumuyla doğrudan ilişkiliyse (örneğin bir savaştaki düşmanın anlık can değeri), genellikle MonoBehaviour içindeki bir C# sınıfı daha uygun olabilir.
  • S: ScriptableObject'leri kaydederken güvenlik endişeleri var mı? Oyuncular verileri değiştirebilir mi?
    C: ScriptableObject'ler build edilmiş oyunda asset olarak bulunduğu için, eğer harici bir serileştirme kullanıp bu serileştirilmiş dosyayı (örneğin JSON) şifrelemezseniz, bilgili oyuncular oyun dosyalarını inceleyerek bu verileri potansiyel olarak değiştirebilir. Temel ScriptableObject asset'lerinin kendisi doğrudan değiştirilemez (çünkü oyun çalışırken bunlar Asset paketinin bir parçasıdır), ancak eğer runtime'da kaydedilen harici bir dosyayı kullanıyorsanız, o dosyanın içeriği manipüle edilebilir. Ciddi güvenlik gerektiren (örneğin çok oyunculu oyunlarda hile önleme) veriler için, verileri şifrelemek, karma değeri (hash) kontrolü yapmak veya güvenilir sunucu tarafında doğrulamak gibi ek önlemler almanız gerekebilir.
  • S: Bir ScriptableObject içinde başka bir ScriptableObject'e referans verebilir miyim?
    C: Evet, kesinlikle! Bu, ScriptableObjects'ın en güçlü özelliklerinden biridir ve karmaşık veri yapıları oluşturmanın temelini oluşturur. Örneğin, bir CharacterClassSO (karakter sınıfı) içinde, o sınıfa özel yetenekleri tanımlayan bir List tutabilirsiniz. Bu, veriler arasında ilişkiler kurmanıza ve Editör üzerinden birbiriyle bağlantılı veri asset'leri ağacı oluşturmanıza olanak tanır.
  • S: Unity'nin kaydetme sistemiyle (Serialization System) ScriptableObjects'ın ilişkisi nedir?
    C: ScriptableObjects, Unity'nin serileştirme sistemine derinlemesine entegre edilmiştir. Bir ScriptableObject asset'i oluşturulduğunda, Unity onun içindeki serileştirilebilir alanları (public alanlar veya [SerializeField] ile işaretlenmiş private/protected alanlar) otomatik olarak diskte bir dosya olarak kaydeder. Unity'nin serileştirme sistemi, ScriptableObjects'ı yükleyip boşaltmayı ve Editör'de Inspector aracılığıyla düzenlenmesini mümkün kılar. Bu, ScriptableObjects'ın "Asset" olarak çalışma prensibinin temelini oluşturur.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

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

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.