Unity’de RPG Envanter ve Basit Görev Sistemi Oluşturma

Unity'de RPG oyunları için kapsamlı bir envanter ve basit görev sistemi nasıl oluşturulur? C# ile ScriptableObject kullanarak modüler çözümler geliştirin. Unity Envanter Sistemi kurun.

Rol yapma oyunlarının (RPG) kalbinde, oyuncuların dünyayla etkileşimini sağlayan iki temel mekanik yatar: Envanter Sistemi ve Görev Sistemi. Bu sistemler, oyuncunun ilerlemesini, karakter gelişimini ve oyun deneyimini derinden etkiler. Bu makalede, Unity motorunda C# kullanarak nasıl modüler, genişletilebilir ve yönetilebilir bir envanter ve basit görev sistemi oluşturulacağını adım adım inceleyeceğiz. Amacımız, sadece temel işlevselliği sağlamakla kalmayıp, aynı zamanda gelecekteki genişlemeler için sağlam bir temel oluşturmaktır.

RPG Envanter Sistemi Temelleri

Bir envanter sistemi, oyuncunun topladığı eşyaları depolamasını, düzenlemesini ve kullanmasını sağlayan bir arayüz ve arka plan mekaniğidir. Temel bir Unity Envanter Sistemi için aşağıdaki bileşenlere ihtiyacımız var:

1. Eşya Tanımlaması (Item ScriptableObject)

Her eşyanın kendine özgü özellikleri olmalıdır (adı, açıklaması, ikonu, istiflenebilir olup olmadığı vb.). Bu tür verileri depolamak için Unity’nin ScriptableObject özelliğini kullanmak en iyi yaklaşımlardan biridir. Bu sayede her eşya türü için ayrı bir asset dosyası oluşturabilir ve oyun içi verileri koddan ayırabiliriz.

using UnityEngine;

[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item")]
public class Item : ScriptableObject
{
    public string itemName = "Yeni Eşya";
    public Sprite icon = null;
    public bool isStackable = false;
    public int maxStackSize = 1;
    [TextArea(3, 5)]
    public string description = "Eşya açıklaması.";

    public virtual void Use()
    {
        Debug.Log("Eşya kullanıldı: " + itemName);
        // Bu metot, eşya kullanıldığında yapılacak özel eylemler için ezilebilir.
    }
}

2. Envanter Yönetimi (InventoryManager)

InventoryManager, oyuncunun envanterindeki eşyaları tutmaktan, eklemekten, çıkarmaktan ve kapasiteyi yönetmekten sorumlu ana sınıftır. Basit bir liste veya eşya ve miktarını tutan bir sözlük (`Dictionary`) kullanabiliriz.

using System.Collections.Generic;
using UnityEngine;

public class InventoryManager : MonoBehaviour
{
    public static InventoryManager instance;

    public delegate void OnItemChanged();
    public OnItemChanged onItemChangedCallback;

    public int inventoryCapacity = 20;
    public List<InventorySlot> inventorySlots = new List<InventorySlot>();

    void Awake()
    {
        if (instance != null)
        {
            Debug.LogWarning("Birden fazla InventoryManager örneği bulundu!");
            return;
        }
        instance = this;

        // Envanter slotlarını başlangıçta boş olarak doldur
        for (int i = 0; i < inventoryCapacity; i++)
        {
            inventorySlots.Add(new InventorySlot());
        }
    }

    public bool AddItem(Item item, int amount = 1)
    {
        // İstiflenebilir eşya ekleme mantığı
        if (item.isStackable)
        {
            foreach (InventorySlot slot in inventorySlots)
            {
                if (slot.item == item && slot.amount < item.maxStackSize)
                {
                    slot.amount += amount;
                    if (slot.amount > item.maxStackSize)
                    {
                        // Fazla eşyayı yeni slota veya sonraki slota aktar
                        int remaining = slot.amount - item.maxStackSize;
                        slot.amount = item.maxStackSize;
                        AddItem(item, remaining); // Kalanı tekrar eklemeye çalış
                    }
                    onItemChangedCallback?.Invoke();
                    return true;
                }
            }
        }

        // Yeni slot bulma veya istiflenemeyen eşya ekleme
        foreach (InventorySlot slot in inventorySlots)
        {
            if (slot.item == null)
            {
                slot.item = item;
                slot.amount = amount;
                onItemChangedCallback?.Invoke();
                return true;
            }
        }

        Debug.LogWarning("Envanter dolu: " + item.itemName);
        return false;
    }

    public void RemoveItem(Item item, int amount = 1)
    {
        // Eşya çıkarma mantığı
        for (int i = inventorySlots.Count - 1; i >= 0; i--)
        {
            if (inventorySlots[i].item == item)
            {
                inventorySlots[i].amount -= amount;
                if (inventorySlots[i].amount <= 0)
                {
                    inventorySlots[i].item = null;
                    inventorySlots[i].amount = 0;
                }
                onItemChangedCallback?.Invoke();
                return;
            }
        }
    }
}

[System.Serializable]
public class InventorySlot
{
    public Item item;
    public int amount;

    public InventorySlot()
    {
        item = null;
        amount = 0;
    }
}

Yukarıdaki kodda, InventorySlot sınıfı her bir envanterdeki boş veya dolu slotu temsil eder. onItemChangedCallback ise envanterde bir değişiklik olduğunda UI’ı güncellemek için kullanılacak bir event‘tir.

Basit Görev Sistemi Temelleri

Görev sistemi, oyuncuya hedefler vererek hikaye anlatımını ve ilerlemeyi sağlayan bir mekaniktir. Basit bir görev sistemi için aşağıdaki bileşenlere ihtiyacımız var:

1. Görev Tanımlaması (Quest ScriptableObject)

Her görevin adı, açıklaması, hedefleri ve ödülleri olmalıdır. Yine ScriptableObject kullanarak görev verilerini kolayca yönetebiliriz.

using UnityEngine;
using System.Collections.Generic;

public enum QuestStatus { NotStarted, Active, Completed, ClaimedReward }

[CreateAssetMenu(fileName = "New Quest", menuName = "Quest/Quest")]
public class Quest : ScriptableObject
{
    public string questName = "Yeni Görev";
    [TextArea(3, 5)]
    public string description = "Görev açıklaması.";
    public List<QuestGoal> goals = new List<QuestGoal>();
    public List<Item> rewards = new List<Item>();
    public int experienceReward = 0;

    [HideInInspector] // Oyun içinde durum takibi için kullanılır, Inspector'da görünmez
    public QuestStatus currentStatus = QuestStatus.NotStarted;

    public bool IsCompleted()
    {
        foreach (QuestGoal goal in goals)
        {
            if (!goal.IsGoalCompleted())
            {
                return false;
            }
        }
        return true;
    }

    public void StartQuest()
    {
        currentStatus = QuestStatus.Active;
        foreach (QuestGoal goal in goals)
        {
            goal.InitializeGoal();
        }
        Debug.Log(questName + " görevi başlatıldı.");
    }

    public void CompleteQuest()
    {
        if (IsCompleted() && currentStatus == QuestStatus.Active)
        {
            currentStatus = QuestStatus.Completed;
            Debug.Log(questName + " görevi tamamlandı!");
            // Görev tamamlama event'ini tetikleyebiliriz
        }
    }

    public void ClaimRewards()
    {
        if (currentStatus == QuestStatus.Completed)
        {
            foreach (Item rewardItem in rewards)
            {
                InventoryManager.instance.AddItem(rewardItem);
            }
            // Oyuncuya deneyim puanı ver
            Debug.Log(experienceReward + " deneyim puanı kazanıldı.");
            currentStatus = QuestStatus.ClaimedReward;
            Debug.Log(questName + " ödülleri alındı.");
        }
    }
}

// Görev hedefleri için temel sınıf
[System.Serializable]
public abstract class QuestGoal : ScriptableObject
{
    public string goalDescription;
    public bool isCompleted = false;
    public int currentAmount = 0;
    public int requiredAmount = 1;

    public virtual void InitializeGoal() { }
    public virtual bool IsGoalCompleted() { return isCompleted; }
}

// Örnek: Belirli sayıda eşya toplama hedefi
[CreateAssetMenu(fileName = "New Collect Goal", menuName = "Quest/Goal/Collect")]
public class CollectQuestGoal : QuestGoal
{
    public Item itemToCollect;

    public override void InitializeGoal()
    {
        isCompleted = false;
        currentAmount = 0;
        // Envanter değişikliklerini dinlemeye başla
        InventoryManager.instance.onItemChangedCallback += CheckInventory;
    }

    public override bool IsGoalCompleted()
    {
        return isCompleted;
    }

    public void ItemCollected(Item item)
    {
        if (item == itemToCollect)
        {
            currentAmount++;
            if (currentAmount >= requiredAmount)
            {
                isCompleted = true;
                InventoryManager.instance.onItemChangedCallback -= CheckInventory; // Hedef tamamlandı, dinlemeyi bırak
            }
        }
    }

    // Envanterde belirli bir eşyadan yeterli miktarda olup olmadığını kontrol et
    private void CheckInventory()
    {
        int count = 0;
        foreach (InventorySlot slot in InventoryManager.instance.inventorySlots)
        {
            if (slot.item == itemToCollect)
            {
                count += slot.amount;
            }
        }

        currentAmount = count;
        if (currentAmount >= requiredAmount)
        {
            isCompleted = true;
            // InventoryManager.instance.onItemChangedCallback -= CheckInventory; // Bu kısım RemoveItem durumunda sorun çıkarabilir, InitializeGoal'da tekrar bağlanmalı.
        } else {
            isCompleted = false;
        }
    }
}

QuestGoal soyut sınıfı, farklı görev hedefleri (eşya toplama, düşman öldürme, belirli bir yere gitme) için temel bir yapı sağlar. Her hedef türü bu sınıftan türetilerek kendi özel mantığını uygulayabilir.

2. Görev Yöneticisi (QuestManager)

QuestManager, oyuncunun aktif görevlerini takip etmekten, görev ilerlemesini güncellemekten ve tamamlanan görevleri yönetmekten sorumludur.

using System.Collections.Generic;
using UnityEngine;

public class QuestManager : MonoBehaviour
{
    public static QuestManager instance;

    public List<Quest> activeQuests = new List<Quest>();

    void Awake()
    {
        if (instance != null)
        {
            Debug.LogWarning("Birden fazla QuestManager örneği bulundu!");
            return;
        }
        instance = this;
    }

    void Update()
    {
        CheckQuestCompletion();
    }

    public void AcceptQuest(Quest quest)
    {
        if (!activeQuests.Contains(quest))
        {
            activeQuests.Add(quest);
            quest.StartQuest();
            Debug.Log(quest.questName + " görevi kabul edildi.");
            // UI'ı güncellemek için event tetiklenebilir
        }
    }

    public void CompleteQuest(Quest quest)
    {
        if (activeQuests.Contains(quest) && quest.IsCompleted() && quest.currentStatus == QuestStatus.Active)
        {
            quest.CompleteQuest();
            // UI'ı güncellemek için event tetiklenebilir
        }
    }

    public void ClaimQuestRewards(Quest quest)
    {
        if (activeQuests.Contains(quest) && quest.currentStatus == QuestStatus.Completed)
        {
            quest.ClaimRewards();
            activeQuests.Remove(quest); // Ödüller alındıktan sonra aktif görevlerden kaldır
            // UI'ı güncellemek için event tetiklenebilir
        }
    }

    private void CheckQuestCompletion()
    {
        foreach (Quest quest in activeQuests)
        {
            if (quest.currentStatus == QuestStatus.Active && quest.IsCompleted())
            {
                quest.CompleteQuest();
            }
        }
    }
}

Pratik İpuçları ve En İyi Uygulamalar

1. Modüler Tasarım ve ScriptableObject Kullanımı

Yukarıdaki örneklerde gördüğünüz gibi, ScriptableObject‘ler eşya ve görev verilerini koddan ayırmak için mükemmel bir yöntemdir. Bu, tasarımcıların kod yazmadan yeni eşyalar veya görevler oluşturmasına olanak tanır ve kodun daha temiz ve yönetilebilir olmasını sağlar. Ayrıca, bu asset’leri kolayca çoğaltabilir ve değiştirebilirsiniz.

2. Event Tabanlı İletişim (Observer Pattern)

InventoryManager içindeki onItemChangedCallback gibi delegate‘ler ve event‘ler, farklı sistemler arasında bağımsız bir iletişim kurmanın anahtarıdır. Bu, UI’ın envanterdeki değişiklikleri doğrudan bilmesini sağlar, ancak UI sınıfı InventoryManager‘ın iç detaylarına bağımlı olmaz. Bu tür bir ayrım (decoupling), kodun daha esnek ve bakımı kolay olmasını sağlar. Örneğin, görev sistemi de envanterdeki değişiklikleri dinleyerek toplama görevlerini güncelleyebilir.

3. Kalıcılık (Persistence) için Basit Bir Yaklaşım

Oyun kaydetme ve yükleme, bu sistemlerin önemli bir parçasıdır. En basit haliyle, envanter ve görev verilerini JSON formatında serileştirebilirsiniz. Her InventorySlot ve Quest‘in mevcut durumunu (hangi eşya, miktarı, görev durumu, hedeflerin ilerlemesi) bir veri sınıfına aktarabilir ve bu sınıfı JSON’a dönüştürerek kaydedebilirsiniz. Yükleme sırasında bu JSON’u okuyup ilgili ScriptableObject‘leri referans alarak sistemleri yeniden kurabilirsiniz.

Yaygın Hatalar ve Çözümleri

1. Her Şeyi Tek Bir Sınıfta Toplamak (Monolithic Design)

Hata: Envanter ve görev mantığını tek bir büyük MonoBehaviour sınıfına yazmak, kodun okunmasını, test edilmesini ve genişletilmesini zorlaştırır.
Çözüm: Sorumlulukları ayırın. Eşya verileri için Item, envanter mantığı için InventoryManager, görev verileri için Quest ve görev ilerlemesi için QuestManager gibi ayrı sınıflar ve ScriptableObject‘ler kullanın.

2. UI ve Oyun Mantığını Karıştırmak

Hata: Envanter UI’ını doğrudan InventoryManager içinde güncellemeye çalışmak veya UI elemanlarının doğrudan oyun mantığına erişmesine izin vermek.
Çözüm: Oyun mantığını ve UI’ı birbirinden ayırın. Oyun mantığı (InventoryManager gibi) bir değişiklik olduğunda bir event yayınlamalıdır. UI sınıfları bu event‘leri dinlemeli ve yalnızca UI’ı güncellemeli. Bu, her iki sistemin de bağımsız olarak geliştirilmesine ve değiştirilmesine olanak tanır.

3. Esneklik Eksikliği

Hata: Yeni eşya türleri veya görev hedefleri eklemenin çok fazla kod değişikliği gerektirmesi.
Çözüm: ScriptableObject‘leri ve miras (inheritance) yapısını akıllıca kullanın. Örneğin, Item sınıfından türeyen PotionItem veya EquipmentItem gibi alt sınıflar oluşturabilirsiniz. Aynı şekilde, QuestGoal soyut sınıfından türeyen KillGoal, CollectGoal gibi hedefler tanımlayarak sistemi kolayca genişletebilirsiniz. Bu sayede yeni bir özellik eklemek genellikle sadece yeni bir ScriptableObject oluşturmak anlamına gelir.

Performans ve Optimizasyon Notları

Genellikle bir Unity Envanter Sistemi ve görev sistemi, özellikle küçük ve orta ölçekli oyunlarda performans sorunlarına yol açmaz. Ancak, çok büyük envanterler veya aynı anda yüzlerce aktif görev olduğunda dikkat edilmesi gereken bazı noktalar vardır:

  • UI Güncellemeleri: Envanter UI’ını her karede (Update metodunda) güncellemek yerine, yalnızca envanterde bir değişiklik olduğunda (onItemChangedCallback gibi event’ler aracılığıyla) güncelleyin. Bu, gereksiz CPU döngülerini önler.
  • Veri Yapıları: Çok sayıda eşya içeren envanterler için List<T> yerine Dictionary<TKey, TValue> kullanmak, belirli bir eşyayı ararken performansı artırabilir. Ancak, envanter slotları sabitse ve sıkça indeksle erişiliyorsa List de gayet uygun olabilir.
  • Görev İlerlemesi Kontrolü: QuestManager‘ın Update metodunda tüm aktif görevlerin tamamlanıp tamamlanmadığını kontrol etmek, çok fazla görev olduğunda maliyetli olabilir. Bunun yerine, görev hedeflerinin kendilerini tetikleyen olaylara (eşya toplama, düşman ölümü) abone olmasını sağlayarak yalnızca ilgili olay gerçekleştiğinde kontrol yapılmasını sağlayabilirsiniz.

Bu makalede ele aldığımız prensipler ve kod örnekleri, Unity’de sağlam bir Unity Envanter Sistemi ve görev sistemi oluşturmanız için bir başlangıç noktası sunmaktadır. Modülerliği, esnekliği ve genişletilebilirliği ön planda tutarak, oyununuz büyüdükçe bile sisteminizi kolayca yönetebilir ve geliştirebilirsiniz. Unutmayın, iyi bir sistem tasarımı, uzun vadede size zaman ve emek kazandıracaktır.

Leave a Reply

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