Giriş: Ölçeklenebilir Bir Geleceğin Tasarımı
Bu doküman, "Handayız" adlı dijital kart oyununun Faz 1: Çekirdek Prototip aşaması için eksiksiz teknik şartnameyi sunmaktadır. Bu fazın temel hedefi, projenin gelecekteki tüm gelişim aşamalarını (çok oyunculu mod, 3D görseller, karmaşık kart yetenekleri vb.) destekleyecek; sağlam, ölçeklenebilir ve bakımı kolay bir yazılım mimarisi kurmaktır. Faz 1'in kapsamı, tek bir bilgisayar üzerinde iki oyuncuyla ("hot-seat") oynanabilen temel bir prototip oluşturmakla sınırlı olsa da, bu dokümanda detaylandırılan mimari, projenin nihai vizyonu göz önünde bulundurularak tasarlanmıştır. Bu yaklaşım, gelecekte ortaya çıkabilecek teknik borcu en aza indirir ve prototipin "kullan-at" bir çalışma olmasını engelleyerek, nihai ürünün ilk ve en sağlam katmanı olmasını sağlar. Bu hedefe ulaşmak için modern, olay tabanlı (event-driven) ve veri odaklı (data-oriented) bir paradigma benimsenecektir.
1. Çekirdek Mimari Çerçevesi
1.1. Olay Tabanlı, Veri Odaklı Paradigma: "Neden?" Sorusunun Cevabı
Projede, temel sistemler arasındaki bağımlılığı ortadan kaldırmak için bir Olay Tabanlı Mimari (Event-Driven Architecture - EDA) benimsenecektir. Kart oyunları doğası gereği, tek bir eylemin birden çok sistemi etkilediği karmaşık etkileşimlere sahiptir. Örneğin, bir kartın oynanması; oyuncunun elini, oyun masasındaki durumu, kullanıcı arayüzünü (UI) ve potansiyel olarak diğer kartların yeteneklerini aynı anda tetikleyebilir. Bu sistemleri birbirine doğrudan referanslarla bağlamak ("spaghetti code" olarak bilinen karmaşık ve kırılgan bir yapıya yol açar), kodun bakımını ve genişletilmesini son derece zorlaştırır. EDA, sistemlerin bir "Olay Yolu (Event Bus)" üzerinden dolaylı olarak iletişim kurmasını sağlayarak modülerliği, test edilebilirliği ve ölçeklenebilirliği teşvik eder.
Bu mimariyi Unity içerisinde en etkin şekilde uygulamak için, ScriptableObject tabanlı bir olay sistemi kullanılacaktır. Geleneksel C# olayları (events), özellikle sahne geçişlerinde veya nesneler yok edildiğinde dinleyicilerin (listeners) düzgün bir şekilde kaydının silinmemesi durumunda bellek sızıntılarına ve hatalara yol açabilir. ScriptableObject tabanlı olaylar ise bu sorunu ortadan kaldırır. Bu olaylar, proje içerisinde birer "asset" olarak var olurlar ve sahneden bağımsızdırlar. Bu yaklaşım, aynı zamanda tasarımcıların Unity editörü içerisinden olayları görmesine, referans vermesine ve hatta test amaçlı tetiklemesine olanak tanıyarak tüm ekibin oyunun veri akışını daha kolay anlamasını ve hata ayıklamasını sağlar. Bu, özellikle geliştiricinin mevcut öğrenme süreci ve proje planı göz önüne alındığında büyük bir avantajdır.
1.2. Üst Düzey Sistem Etkileşim Diyagramı
Aşağıdaki bileşen diyagramı, projenin ana sistemleri arasındaki iletişim akışını görselleştirmektedir. Diyagramda görüleceği üzere, GameManager, TurnManager, UIManager ve InputReader gibi ana sistemler birbirleriyle doğrudan bir bağlantı kurmazlar. Tüm iletişim, merkezi bir Oyun Olay Yolu (Game Event Bus) üzerinden dolaylı olarak gerçekleşir. Bu, sistemlerin birbirinden habersiz (decoupled) bir şekilde çalışmasını garanti altına alan temel mimari prensibimizdir.
Diyagram 1: Sistemlerin Olay Yolu (Event Bus) üzerinden dolaylı iletişimini gösteren üst düzey mimari şeması.
1.3. Sadece Faz 1 İçin Değil, Faz 3 İçin Mimari Tasarlamak
Bu projedeki en önemli risklerden biri, Faz 1 için hızlı ve basit bir prototip oluşturup, ilerleyen fazlarda çok oyunculu (multiplayer) ve 3D gibi karmaşık özellikler ekleneceği zaman bu prototipin tamamen çöpe atılması gerekliliğidir. Bu teknik tasarım dokümanı, bu riski bilinçli olarak ortadan kaldırmak üzere hazırlanmıştır.
Bu stratejinin arkasındaki mantık akışı şu şekildedir:
- Faz 1 iş planı, tek bilgisayarda oynanan basit bir prototipi hedeflemektedir. Bu durumda bir geliştirici, sistemleri birbirine doğrudan bağlama eğiliminde olabilir. Örneğin, puan güncellendiğinde
GameManager.Instance.UIManager.UpdateScore()
gibi bir fonksiyonu doğrudan çağırabilir. Bu, başlangıçta hızlı bir çözümdür. - Ancak, projenin uzun vadeli vizyonunu içeren Oyun Tasarım Dokümanı (GDD), sunucu-yetkili (server-authoritative) bir çok oyunculu mimariyi şart koşmaktadır. Bu mimaride, bir oyuncunun (client) arayüzü doğrudan güncellemesi mümkün değildir. Oyuncu, bir eylem talebini sunucuya gönderir; sunucu bu eylemi doğrular ve sonucunu tüm oyunculara yayınlar.
- İlk adımda bahsedilen basit ve sıkı sıkıya bağlı (tightly-coupled) yaklaşım, ikinci adımda gereken mimariyle temelden çelişir. Bu durum, Faz 1'de harcanan 8 haftalık geliştirme süresinin boşa gitmesine ve kodun tamamen yeniden yazılmasına neden olur.
- Buna karşılık, bu dokümanda önerilen olay tabanlı mimari, bu iki faz arasında kusursuz bir köprü görevi görür. Faz 1'de, ScoreManager yerel olarak bir
ScoreUpdated
olayı yayınlar ve UIManager bu olayı dinleyerek arayüzü günceller. Faz 3'e geçildiğinde ise, istemci tarafındaki ScoreManager mantığı, sunucudan gelen birScoreUpdated
ağ mesajını dinleyen bir NetworkClient ile değiştirilir. En kritik nokta şudur: UIManager'ın kodunda hiçbir değişiklik yapılmasına gerek kalmaz. Arayüz, kimin yayınladığına bakmaksızın sadeceScoreUpdated
olayını dinlemeye devam eder.
Sonuç olarak, başlangıçta biraz daha karmaşık görünen bu olay tabanlı mimariyi benimsemek, Faz 1'de yapılan işin tek kullanımlık değil, projenin geleceği için sağlam ve kalıcı bir temel olmasını sağlar. Bu TDD'nin en temel stratejik değeri budur.
Tablo 1: Çekirdek Oyun Olayları (ScriptableObject Tabanlı)
Olay Adı (SO Asset) | Veri Yükü (Payload) | Açıklama | Birincil Yayıncı(lar) | Birincil Abone(ler) |
---|---|---|---|---|
GameSetupStarted | void | Yeni bir raundun başladığını ve tüm sistemlerin kurulum için hazırlanması gerektiğini bildirir. | GameManager | DeckManager, UIManager, PlayerManager |
TurnStarted | PlayerID | Sırası gelen oyuncunun kimliğini belirterek yeni bir turun başladığını duyurur. | TurnManager | UIManager, InputReader, GameplayLogic |
CardDrawn | PlayerID, CardData | Bir oyuncunun eline yeni bir kart çektiğini bildirir. | DeckManager | UIManager, PlayerState |
CardPlayed | PlayerID, CardData | Bir oyuncunun masaya başarılı bir şekilde kart oynadığını bildirir. | GameplayLogic | UIManager, ScoreManager, AbilityManager (Faz 2+) |
SetPlayed | PlayerID, List<CardData> | Bir oyuncunun masaya bir set (örn: 3'lü Günahkar) oynadığını bildirir. | GameplayLogic | UIManager, AbilityManager (Faz 2+) |
ScoreUpdated | PlayerID, int newTotalScore | Bir oyuncunun toplam puanının değiştiğini ve yeni değerini bildirir. | ScoreManager | UIManager |
RoundEnded | void | Raundun sona erme koşullarından birinin gerçekleştiğini duyurur. | GameplayLogic | GameManager, ScoreManager |
2. Veri Mimarisi ve Varlık Yönetimi
2.1. CardData ScriptableObject: Tek Gerçeklik Kaynağı
Projedeki tüm kartlar (~145 adet), CardData adında bir ScriptableObject asset'i olarak temsil edilecektir. Bu yapı, projenin en kritik veri yapısıdır. ScriptableObject'tan kalıtım alması, oyun tasarımcılarının Unity editörü içinde, kod yazmadan yeni kartlar oluşturmasına, mevcut kartları düzenlemesine ve dengelemesine olanak tanır. Her bir kart, kendi CardData.asset
dosyasına sahip olacak ve oyunun "tek gerçeklik kaynağı" (single source of truth) olarak işlev görecektir.
Ancak, kartların karmaşık yeteneklerini yönetmek için basit bir metin alanı (string abilityDescription) kullanmak, projenin geleceği için büyük bir tuzaktır. Kural kitapçığında "3 veya daha fazla bu Günahkar'a sahipsen..." veya "Bu Günahkar Kilisene ilk girdiğinde..." gibi koşullu ve tetiklemeli yetenekler bulunmaktadır. Bu metinleri oyun sırasında ayrıştırmak (parse etmek) hem karmaşık hem de hataya açık bir süreçtir. Alternatif olarak, tüm yetenekleri GameplayLogic sınıfı içinde dev bir switch-case bloğu ile kodlamak ise ölçeklenemez bir anti-desendir.
Bu sorunu aşmak için, yeteneklerin kendisi de veri olarak ele alınacaktır. CardData sınıfı, bir List<AbilityEffect>
içerecektir. AbilityEffect
, aşağıdaki gibi alanlara sahip, serileştirilebilir (serializable) bir C# sınıfı olacaktır:
- TriggerType (enum): Yeteneğin ne zaman tetikleneceğini belirtir (örn: OnPlay, OnDiscard, OnSetCount, OnRoundEnd).
- Condition (class): Yeteneğin çalışması için gereken koşulları tanımlar (örn: SetCountCondition sınıfı, targetSinType ve requiredCount gibi alanlar içerebilir).
- Effect (class): Koşul sağlandığında hangi etkinin uygulanacağını tanımlar (örn: DrawCardEffect sınıfı, amount alanı içerebilir).
Faz 1 kapsamında, bu AbilityEffect yapısı sadece veri olarak oluşturulacak ve GameplayLogic tarafından işlenmeyecektir. Ancak bu altyapı, Faz 2'de karmaşık yeteneklerin uygulanması için sağlam bir zemin hazırlar. Geliştirici, bu listeyi işleyen bir AbilityManager sistemi yazdığında, yeni yetenekler eklemek sadece yeni veri asset'leri oluşturmak kadar kolay olacaktır. Bu yaklaşım, büyük bir yeniden yapılandırma (refactoring) ihtiyacını ortadan kaldırır ve GDD'de belirtilen JSON benzeri yetenek yapısı vizyonuyla da uyumludur.
Tablo 2: CardData.cs Alan Tanımları
Alan Adı | C# Tipi | Inspector Açıklaması (Tooltip) | Amaç ve Açıklama |
---|---|---|---|
cardID | string | Benzersiz kimlik (örn: 'sinner_wrathful_01'). Asla değiştirilmemelidir. | Veritabanı, ağ iletişimi ve kaydetme/yükleme işlemleri için benzersiz bir anahtar. |
cardName | string | Kartın oyuncuya gösterilecek adı. | Örn: "Öfkeli", "Lucifer", "Vaftiz". |
cardType | CardType (enum) | Kartın ana kategorisi: Günahkar, Şeytan, Katil, Ayin, İyiliksever. | Oyun mantığının kartları ayırt etmesi için temel sınıflandırma. |
sinType | SinType (enum) | Sadece 'Günahkar' kartları için günah türü. Örn: Öfke, Oburluk. | Günahkar setlerini oluşturmak için kullanılır. 'None' olarak ayarlanabilir. |
gender | GenderType (enum) | Sadece 'Günahkar' kartları için cinsiyet: Erkek, Kadın. Diğerleri için Cinsiyetsiz. | Bazı Ayin kartlarının (örn: Matrimony) mekanikleri için gereklidir. |
soulValue | int | Kartın temel ruh/puan değeri. Negatif olabilir. | Raund sonunda puanlama için kullanılır. |
cardArt | Sprite | Kartın ön yüzünde gösterilecek 2D görsel. | Arayüzde kartın görsel temsili. |
abilityDescription | string | Kartın yeteneğinin oyuncuya gösterilecek metni. | Oyuncunun kartın ne işe yaradığını anlaması için açıklama metni. |
cardAbilities | List<AbilityEffect> | Bu kartın sahip olduğu programatik özel yeteneklerin listesi. | Faz 2 ve sonrası için ölçeklenebilir yetenek sisteminin veri temeli. |
2.2. Destekleyici Veri Yapıları
DeckData.cs (ScriptableObject): Bu sınıf, bir List<CardData>
içerir. Bu yapı, tasarımcıların farklı oyun modları veya oyuncu sayıları için "2 Kişilik Deste", "Tam Deste" gibi farklı deste konfigürasyonlarını proje içinde asset olarak oluşturmalarını sağlar. Oyun başladığında, DeckManager ilgili DeckData asset'ini kullanarak oyun destesini oluşturur.
PlayerState.cs (Plain Old C# Object - POCO): Bu sınıf, bir MonoBehaviour veya ScriptableObject değildir; saf bir C# sınıfıdır. Oyun başladığında her oyuncu için bir örneği (instance) oluşturulur. Oyuncunun o anki durumunu temsil eden geçici verileri tutar: List<CardData> hand
(eldeki kartlar), List<CardSet> playedSets
(oynanmış setler), int currentScore
(mevcut puan), seçilen DemonCard ve MurdererCard referansları vb. Bu yapı, statik kart tanımlarını (CardData) dinamik ve geçici oyuncu durumundan net bir şekilde ayırır, bu da veri yönetimini temiz ve anlaşılır kılar.
2.3. Proje Varlık Organizasyonu
Proje içerisinde oluşturulacak ~145 CardData asset'i ve diğer ScriptableObject'ları yönetmek için başından itibaren katı bir klasör hiyerarşisi uygulanmalıdır. Bu, özellikle ekip büyüdüğünde veya proje karmaşıklaştığında varlık kaosunu önlemek için kritik öneme sahiptir.
Assets/
└── Game/
├── _Scenes/
├── Art/
├── Audio/
└── Scripts/
│ ├── Core/ (Managers, State Machine)
│ ├── Data/ (CardData, DeckData, PlayerState)
│ ├── UI/
│ └── Input/
└── ScriptableObjects/
├── Cards/
│ ├── Sinners/
│ ├── Demons/
│ ├── Murderers/
│ ├── Sacraments/
│ └── GoodSamaritans/
├── Decks/
└── Events/
├── GameStateEvents/
└── GameplayEvents/
3. Çekirdek Sistemler ve Yönetici Sınıfları
3.1. GameManager: Ana Durum Kontrolcüsü
GameManager, oyunun en üst düzeydeki akışını yöneten orkestra şefidir. Sorumluluğu, oyunun bir durumdan diğerine geçişini sağlamaktır (örneğin, Ana Menü'den Oyun Kurulumu'na, oradan da Oyuncu Turu'na geçiş).
Tasarım Deseni (Design Pattern): GameManager'ın durumlarını yönetmek için Durum (State) tasarım deseni kullanılacaktır. Ancak bu, basit bir enum ve switch-case yapısıyla değil, sınıflar ve bir arayüz (IState) kullanılarak daha sofistike bir şekilde uygulanacaktır. switch-case yapısı, yeni durumlar eklendikçe hızla büyüyüp yönetilemez hale gelir ve Açık/Kapalı Prensibi'ni (Open/Closed Principle) ihlal eder. Sınıf tabanlı bir yaklaşım ise her bir durumun mantığını kendi sınıfı içinde (örn: GameSetupState, PlayerTurnState, RoundScoringState) kapsülleyerek sistemi temiz, anlaşılır ve kolayca genişletilebilir hale getirir.
Sorumluluklar: GameManager'ın tek sorumluluğu, oyunun genel durumunu yönetmektir. Oyun mantığını kendisi içermez; bu görevi o anki aktif durum nesnesine devreder. Örneğin, PlayerTurnState aktifken, turla ilgili detayları TurnManager yönetir.
Uygulama: GameManager, private IState currentState;
alanına ve public void TransitionToState(IState nextState)
metoduna sahip olacaktır. Bu metot, mevcut durumun Exit() metodunu, ardından yeni durumun Enter() metodunu çağırarak kontrollü bir geçiş sağlar. GameManager, diğer sistemlerin kolayca erişebilmesi için bir Singleton olarak tasarlanacaktır. Ancak bu, sahneye bağımlı olmayan, "tembel başlatmalı" (lazy initialization) bir Singleton olacaktır ki bu da modülerlik hedefimizle uyumludur.
Diyagram 2: GameManager'ın Durum Makinesi (State Machine) akış şeması.
Tablo 3: GameManager Durum Tanımları (IState Uygulamaları)
Durum Adı (Sınıf) | Giriş Mantığı (Enter()) | Çekirdek Mantık (Update()) | Çıkış Mantığı (Exit()) | Geçiş Yaptığı Durumlar (ve Koşulları) |
---|---|---|---|---|
GameSetupState | GameSetupStarted olayını yayınlar. PlayerManager'ı oyuncu nesnelerini oluşturması için tetikler. | Diğer yöneticilerin kurulumu bitirmesini bekler (örneğin, DeckManager'dan gelen SetupComplete olayını dinler). | - | PlayerTurnState (Tüm sistemler hazır olduğunda). |
PlayerTurnState | TurnManager'a sıradaki oyuncuyla turu başlatmasını söyler. TurnStarted olayı yayınlanır. | Oyuncunun turunu bitirmesini bekler. RoundEnded olayını dinler. | - | PlayerTurnState (Sıradaki oyuncuyla devam eder) veya RoundScoringState (Raund bitiş koşulu sağlandığında). |
RoundScoringState | ScoringStarted olayını yayınlar. ScoreManager'ı puanları hesaplaması için tetikler. | Puanlama ve animasyonların bitmesini bekler. | Oyun tahtasını ve oyuncu alanlarını temizler (resetler). | GameSetupState (Toplam puan < 100 ise) veya GameEndState (Toplam puan >= 100 ise). |
GameEndState | Kazanan oyuncuyu belirler ve GameEnded olayını yayınlar. UI'da kazanan ekranını gösterir. | Oyuncunun menüye dönme girdisini bekler. | - | MainMenuState (Oyuncu menüye dönmeyi seçtiğinde). |
3.2. TurnManager: Oyun Akışı Kondüktörü
TurnManager, GameManager'a bağlı ikincil bir yöneticidir. GameManager PlayerTurnState durumundayken aktif hale gelir. Bir oyuncunun turunun katı sırasını yönetmekle sorumludur: 1. Kart Çek, 2. Kart Oyna, 3. Kart At. Hangi oyuncunun sırası olduğunu, bir sonraki oyuncunun kim olacağını ve oyuncunun turun hangi aşamasında olduğunu takip eder. GameSetupStarted olayını dinleyerek oyuncu listesini alır ve TurnStarted ile TurnEnded gibi olayları yayınlayarak oyunun geri kalanını bilgilendirir.
3.3. UIManager: Reaktif ve Bağımsız Arayüz
UIManager tamamen reaktif bir yapıda olacaktır. Bu, hiçbir oyun mantığı sınıfının UIManager'ı doğrudan çağırmayacağı anlamına gelir (örneğin, UIManager.UpdateScore(10)
gibi bir kod yazılamaz). Bu, mimarinin en temel kurallarından biridir. Bunun yerine, UIManager sadece oyun olaylarına abone olur.
Uygulama: UIManager, ScoreUpdated, TurnStarted, CardDrawn gibi ScriptableObject tabanlı olaylara referanslar tutar. OnEnable metodunda bu olaylara kendi metodlarını (örn: OnScoreUpdated(PlayerID player, int newScore)) dinleyici olarak kaydeder ve OnDisable metodunda bu kayıtları siler. Bir olay yayınlandığında, UIManager ilgili metodu çalıştırarak arayüzü günceller. Bu yaklaşım, UI'ı oyun mantığından tamamen ayırır, bu da UI ve oyun mantığı geliştirmelerinin paralel olarak yürütülmesine olanak tanır ve test edilebilirliği artırır. Bu yapı, Unity'nin UI için önerdiği Model-View-Presenter (MVP) veya Model-View-ViewModel (MVVM) gibi modern desenlerle uyumludur.
4. Oyun Akışı ve Mantık Uygulaması
4.1. Bir Oyuncu Turunun Sıra Diyagramı
Aşağıdaki UML Sıra Diyagramı, bir oyuncunun en temel eylemlerinden biri olan "Set Oynama" eyleminin, girdiden sonuca kadar olan tüm yaşam döngüsünü ve sistemler arası etkileşimi göstermektedir. Bu diyagram, olay tabanlı mimarinin pratikte nasıl çalıştığını net bir şekilde ortaya koyar.
Diyagram 3: "Set Oynama" eyleminin olay tabanlı mimari içindeki akışını gösteren sıra diyagramı.
4.2. Çekirdek Kural Uygulama Mantığı (Faz 1)
Bu bölümde, Faz 1 prototipinde uygulanması gereken temel oyun kurallarının C# kod yapıları detaylandırılmaktadır.
Set Doğrulama (IsValidSet) Algoritması
// GameplayLogic.cs içinde bir metot
public bool IsValidSet(List cardsToPlay, PlayerState currentPlayer)
{
// Kural 1: Minimum set boyutu kontrolü.
// Not: Slothful yeteneği bu sayıyı 2'ye düşürebilir (Faz 2'de eklenecek).
if (cardsToPlay.Count < 3) return false;
// Kural 2: Setin tutarlılık kontrolü.
SinType firstSinnerType = SinType.None;
int deceiverCount = 0;
// Set içindeki ilk gerçek günahkar türünü bul.
foreach (var card in cardsToPlay)
{
if (card.cardType == CardType.Sinner && card.sinType != SinType.Deceiver)
{
firstSinnerType = card.sinType;
break;
}
if (card.sinType == SinType.Deceiver)
{
deceiverCount++;
}
}
// Eğer sette hiç normal günahkar yoksa (sadece Yalancılar varsa), geçersizdir.
if (firstSinnerType == SinType.None && deceiverCount == cardsToPlay.Count) return false;
// Tüm kartların ya Yalancı ya da bulunan ilk günahkar türüyle eşleştiğini kontrol et.
foreach (var card in cardsToPlay)
{
if (card.sinType != SinType.Deceiver && card.sinType != firstSinnerType)
{
return false; // Farklı türde bir günahkar var, set geçersiz.
}
}
// Tüm kontrollerden geçtiyse set geçerlidir.
return true;
}
Raund Sonu Puanlama (CalculateRoundScore) Algoritması
// ScoreManager.cs içinde bir metot
public int CalculateRoundScore(PlayerState player)
{
int totalScore = 0;
// Kural 1: Puan kazanmak için Katil veya Lucifer kontrolü.
bool canScore = player.HasMurdererInChurch() || player.HasDemon("Lucifer");
if (!canScore)
{
// Not: Bu durumda oyuncu bir setini sonraki raunda taşır. Bu mantık GameManager'da yönetilir.
return 0;
}
// Kural 2: Kilisedeki (oynanmış) kartların puanlarını topla.
foreach (var playedSet in player.PlayedSets)
{
foreach (var card in playedSet.Cards)
{
// Not: Belial şeytanı Yalancıların negatif puanını sıfırlar (Faz 2'de eklenecek).
totalScore += card.soulValue;
}
}
// Oynanmış diğer kartları (Ayin, Katil vb.) da ekle.
foreach (var singleCard in player.PlayedSingleCards)
{
totalScore += singleCard.soulValue;
}
// Kural 3: Elde kalan kartların puanlarını çıkar.
foreach (var cardInHand in player.Hand)
{
// Not: İyiliksever kartları Yalancıların negatif puanını iptal eder (Faz 2'de eklenecek).
totalScore -= cardInHand.soulValue;
}
// Not: Diğer Şeytan ve Günahkar bonusları bu aşamada eklenecektir (Faz 2).
// Örn: Beelzebub bonusu, Envious bonusu vb.
return totalScore;
}
5. Oyuncu Girdisi ve Etkileşim
5.1. Input Actions Varlık Konfigürasyonu
Projede Unity'nin yeni Input System paketi kullanılacaktır. Bu sistem, girdileri donanımdan soyutlayarak esnek bir kontrol şeması oluşturmayı sağlar. Proje içinde HandayizControls.inputactions
adında bir asset oluşturulacak ve içinde tek bir "Player" Eylem Haritası (Action Map) tanımlanacaktır.
Tanımlanacak Eylemler (Actions):
- Point (Değer: Vector2): İmlecin ekran üzerindeki pozisyonunu takip eder. Kartların üzerine gelme (hover) ve hedefleme için kullanılır. Fare pozisyonuna bağlanacaktır.
- Click (Tip: Button): Kart seçme, butonlara tıklama ve hedef konumu belirleme gibi tekil eylemler için kullanılır. Farenin sol tuşuna bağlanacaktır.
- Drag (Tip: Pass-through): Kartların sürükle-bırak işlevselliği için kullanılır. Farenin sol tuşunun basılı tutulma durumunu izler.
- Cancel (Tip: Button): Seçimi iptal etme veya bir eylemden geri çıkma için kullanılır. Farenin sağ tuşuna veya 'Escape' tuşuna bağlanabilir.
5.2. InputReader.cs Soyutlama Katmanı
Oyun mantığı sınıflarının (GameplayLogic, UIManager vb.) doğrudan Mouse.current.position
veya Gamepad.current.leftStick
gibi donanıma özgü kodları çağırması, mimari açıdan kritik bir hatadır. Bu, mantığı belirli bir donanıma sıkıca bağlar ve gelecekte yeni kontrolcüler (oyun kumandası, dokunmatik ekran) eklemeyi neredeyse imkansız hale getirir.
Bu sorunun çözümü, bir soyutlama katmanı oluşturmaktır. InputReader.cs adında bir MonoBehaviour sınıfı, bu görevi üstlenir. Bu sınıf, Unity'nin PlayerInput bileşenini kullanarak ham donanım girdilerini dinler ve bunları oyunun anlayabileceği daha soyut, anlamsal olaylara dönüştürür.
Bu yaklaşımın mantık akışı şöyledir:
- Bir oyun mantığı sınıfı,
if (Mouse.current.leftButton.wasPressedThisFrame)
gibi bir kod içerdiğinde, bu kod sadece fare ile çalışır. - Oyun kumandası desteği eklemek istendiğinde, aynı koda
|| Gamepad.current.buttonSouth.wasPressedThisFrame
eklenmesi gerekir. Bu, kodun karmaşıklaşmasına ve Açık/Kapalı Prensibi'nin ihlaline yol açar. - Doğru çözüm, InputReader'ın "Click" eylemini dinlemesidir. Bu eylem, fare sol tuşu veya oyun kumandası 'A' tuşu tarafından tetiklenebilir.
- InputReader, "Click" eylemi gerçekleştiğinde, oyuncunun neden tıkladığını bilmez. Sadece bu girdiyi alır ve
OnClickEvent(Vector2 screenPosition)
gibi daha genel, oyunla ilgili bir olay yayınlar. - Oyunun diğer sistemleri (UIManager gibi), bu soyut olayı dinler ve tıklamanın bir kartın mı, bir butonun mu yoksa boş bir alanın mı üzerinde olduğunu kendileri yorumlar.
Sonuç olarak, InputReader "donanım ne yapıyor?" sorusunu "oyuncunun niyeti ne?" sorusuna çevirir. Oyunun geri kalanı sadece niyetle ilgilenir, donanımla değil. Bu, sistemi son derece esnek ve yeni girdi yöntemleri için kolayca genişletilebilir kılar.
InputReader.cs Örnek Uygulama
using UnityEngine;
using UnityEngine.InputSystem;
// Bu sınıf, PlayerInput bileşeni tarafından gönderilen mesajları alır
// ve bunları ScriptableObject tabanlı olaylara çevirir.
public class InputReader : MonoBehaviour, HandayizControls.IPlayerActions
{
// Inspector'dan atanacak ScriptableObject olayları
[Header("Player Input Events")]
public GameEvent onPointEvent; // İmleç hareketi için
public GameEvent onClickEvent; // Tıklama eylemi için
public GameEvent onDragEvent; // Sürükleme eylemi için
private HandayizControls controls;
private void OnEnable()
{
if (controls == null)
{
controls = new HandayizControls();
controls.Player.SetCallbacks(this);
}
controls.Player.Enable();
}
private void OnDisable()
{
controls.Player.Disable();
}
// IPlayerActions arayüzünden gelen metotlar
public void OnPoint(InputAction.CallbackContext context)
{
// Vector2 pozisyon verisini olayla birlikte yayınla
onPointEvent.Raise(context.ReadValue());
}
public void OnClick(InputAction.CallbackContext context)
{
if (context.phase == InputActionPhase.Performed)
{
// Tıklama olayını yayınla
onClickEvent.Raise();
}
}
public void OnDrag(InputAction.CallbackContext context)
{
// Sürükleme verisini olayla birlikte yayınla
onDragEvent.Raise(context.ReadValue());
}
}
Sonuç
Bu teknik tasarım dokümanında detaylandırılan mimari, "Handayız" projesi için güçlü ve esnek bir temel oluşturmaktadır. Projenin en başından itibaren olay tabanlı ve veri odaklı bir tasarımı önceliklendirerek, Faz 1 prototipinin geçici bir iskele değil, çok katlı bir yapının ilk ve en sağlam katı olması sağlanmaktadır. Bu yaklaşım, gelecekteki geliştirme süreçlerini önemli ölçüde kolaylaştıracak, hata ayıklama süreçlerini basitleştirecek, tasarımcıları güçlendirecek ve projenin çok oyunculu işlevsellik ile zengin 3D etkileşimler gibi nihai vizyonuna ulaşması için net ve ölçeklenebilir bir yol sunacaktır. Bu dokümanın titizlikle takip edilmesi, projenin teknik başarısı için kritik bir öneme sahiptir.