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 (
Updatemetodunda) güncellemek yerine, yalnızca envanterde bir değişiklik olduğunda (onItemChangedCallbackgibi 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>yerineDictionary<TKey, TValue>kullanmak, belirli bir eşyayı ararken performansı artırabilir. Ancak, envanter slotları sabitse ve sıkça indeksle erişiliyorsaListde gayet uygun olabilir. - Görev İlerlemesi Kontrolü:
QuestManager‘ınUpdatemetodunda 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.



