İçeriğe geç
Blog

İki Oyun Bir Kod Tabanı: Paylaşılan Araçların Sınırı

Bir kart oyunu ile bir kule savunma oyunu aynı kayıt, ayarlar, ses ve yerelleştirme altyapısını kullanınca ne kazandık, nerede tıkandık? Paylaşılan kodun işe yaradığı ve ayak bağı olduğu sınırı çiziyoruz.

IIlhan Seyhan6 dk okuma1 görüntülenme
İki Oyun Bir Kod Tabanı: Paylaşılan Araçların Sınırı

Aynı stüdyoda iki farklı tür oyun geliştiriyorsanız, ikinci projede "bunu zaten yazmıştık" cümlesi kaçınılmaz olarak kuruluyor. Bu yazıda Kart Üçlüsü ile Thornguard: Tower Defense arasında paylaştığımız dört altyapı parçasını — kayıt sistemi, ayarlar menüsü, ses yöneticisi ve yerelleştirme — tek tek ele alıp hangisinin gerçekten zaman kazandırdığını, hangisinin iki oyunu birbirine benzetmeye zorladığını anlatıyorum.

Paylaşılan katmanda tam olarak ne var?

Ortak kodu tek bir "core" klasörüne yığmak yerine dört ayrı modül olarak tutuyoruz. Sınır şu: oyun kuralına dokunan hiçbir şey ortak katmana giremez. Kart destesi mantığı, dalga zamanlaması, hasar hesabı — bunların hepsi kendi projesinde kalır. Ortak katmanda kalanlar şunlar:

  • Kayıt/yükleme: diske yazma, bozuk dosyaya karşı koruma, sürüm göçü.
  • Ayarlar: ses seviyeleri, dil, titreşim, erişilebilirlik tercihleri ve bunların kalıcılığı.
  • Ses yöneticisi: kanal karışımı, havuzlanmış ses kaynakları, arka plan müziği geçişleri.
  • Yerelleştirme: anahtar-değer tabloları, eksik anahtar tespiti, çalışma anında dil değişimi.

Bu listede ortak nokta belli: hiçbiri oyuncuya "oyun" olarak görünmüyor. Oyuncunun oynadığı şey değil, oynadığı şeyin etrafındaki kabuk.

Kazanç net olan yer: kayıt sistemi

Kayıt sistemi paylaşımdan en çok kâr eden parça oldu, çünkü zor kısmı oyun türünden tamamen bağımsız: yazma sırasında uygulama kapanırsa dosya bozulmasın, eski sürümden gelen veri okunabilsin, cihaz depolaması dolu olduğunda hata sessizce yutulmasın.

Arayüz kasten dar tutuldu. Ortak katman "neyi" kaydettiğini bilmiyor, sadece "nasıl" kaydedileceğini biliyor:

public interface ISaveStore
{
    void Save<T>(string key, T data);
    bool TryLoad<T>(string key, out T data);
}

public sealed class JsonFileSaveStore : ISaveStore
{
    private readonly string _root;

    public JsonFileSaveStore(string root)
    {
        _root = root;
        Directory.CreateDirectory(_root);
    }

    public void Save<T>(string key, T data)
    {
        var path = Path.Combine(_root, key + ".json");
        var temp = path + ".tmp";

        File.WriteAllText(temp, JsonUtility.ToJson(data));
        File.Copy(temp, path, overwrite: true);
        File.Delete(temp);
    }

    public bool TryLoad<T>(string key, out T data)
    {
        var path = Path.Combine(_root, key + ".json");
        if (!File.Exists(path))
        {
            data = default;
            return false;
        }

        try
        {
            data = JsonUtility.FromJson<T>(File.ReadAllText(path));
            return data != null;
        }
        catch (Exception)
        {
            data = default;
            return false;
        }
    }
}

Önce geçici dosyaya yazıp sonra üzerine kopyalamak sıkıcı bir ayrıntı, ama bir kez çözülünce iki projede de bir daha düşünmedik. TryLoad'ın istisna fırlatmak yerine false döndürmesi de bilinçli: her iki oyunda da "kayıt yoksa varsayılanla başla" davranışı zaten gerekiyordu.

Kritik nokta, veri şemasının paylaşılmaması. Kart oyununun kayıt nesnesiyle kule savunma oyununun kayıt nesnesi tamamen ayrı sınıflar. Ortak olan yalnızca dosyaya inen boru hattı. Bu ayrımı ilk günden yapmasaydık, tek bir PlayerData sınıfı iki oyunun alanlarını da taşıyan bir çöplüğe dönüşürdü.

Yerelleştirme: paylaşımın en az tartışmalı olduğu parça

Yerelleştirme altyapısı da kayıt sistemi gibi türden bağımsız. Anahtar çözümleme, eksik anahtarı ekranda kırmızıyla gösterme, çalışma anında dil değiştirince açık olan tüm metin bileşenlerini yenileme — bunların hiçbiri oyunun kart mı yoksa kule mü olduğuyla ilgilenmiyor.

Buradaki tek gerçek tuzak anahtar isimlendirmesi oldu. Başta ui.confirm, ui.cancel gibi "ortak" anahtarları paylaşılan bir tabloya koyduk. Sorun şu ki iki oyunun tonu aynı değil: birinde "Vazgeç" doğru kelime, diğerinde "Geri Çekil" daha yerinde. Ortak tablo bizi ikisinden birine razı olmaya itti. Çözüm katmanlı tablo oldu: paylaşılan tablo varsayılanı verir, oyun tablosu aynı anahtarı ezebilir. Ezme mekanizması eklendikten sonra ortak tablonun kullanımı azalmadı, aksine arttı — çünkü artık ortak tabloya bir şey koymak taahhüt değil, başlangıç noktası.

Ayak bağı olduğu yer: ayarlar menüsü

Ayarlar menüsünü paylaşmak kâğıt üzerinde en mantıklı fikirdi. İki oyunda da aynı satırlar var: müzik, efekt, dil, titreşim. Paylaşılan menüyü hazır bir prefab ve onu besleyen bir ayar deposu olarak kurduk.

Sorun ilk oyuna özel ayar geldiğinde çıktı. Kart oyununda kart yüzlerinin okunabilirliğiyle ilgili bir tercih gerekiyordu — bu konuya kart arayüzü yazısında ayrıca değinmiştik. Kule savunma tarafındaysa oyun hızıyla ilgili bir tercih anlamlıydı. İki ayarın da ortak menüde yeri yoktu.

İlk refleksimiz yanlıştı: ortak menüye "isteğe bağlı bölüm" kavramı ekledik, sonra bölümün sırasını ayarlayan bir alan, sonra bölümü belirli platformlarda gizleyen bir bayrak. Üç haftada ortak menü, iki oyunun da tam olarak istemediği bir şeyi üreten bir konfigürasyon motoruna dönüştü. Kod satırı sayısı arttı, iki oyun da menüsünden memnun değildi.

Geri aldık. Şimdi ayar deposu paylaşılıyor, ayar ekranı paylaşılmıyor. Depo tarafı gerçekten ortak: bir tercihin okunması, yazılması, varsayılana dönmesi, oyun açılışında uygulanması. Ekran tarafı her projede kendi düzeniyle kuruluyor ve toplamda yazdığımız arayüz kodu, konfigürasyon motorundan daha az.

Buradan çıkardığımız kural şu: görsel düzen paylaşılmaz, davranış paylaşılır. Bir modülde "hangi kutu nerede duracak" sorusuna cevap veren kod varsa, o modül büyük ihtimalle yanlış yerde.

Ses yöneticisi: yarısı kazanç, yarısı sürtünme

Ses yöneticisi ikisinin ortasında kaldı. Kanal karışımı, ses havuzu, aynı efektin aynı karede on kez çalmasını engelleyen kısıtlama — hepsi ortak ve hepsi işe yarıyor.

Ama iki oyunun ses ihtiyacı aynı yoğunlukta değil. Kart oyununda aynı anda çalan ses sayısı bir elin parmaklarını geçmiyor; oyuncu her sesi ayrı ayrı duyuyor ve zamanlama önemli. Kule savunma tarafında ise onlarca birim aynı anda ateş edebiliyor; orada mesele tek sesin netliği değil, kalabalığın kulak tırmalamaması. Ortak yönetici başlangıçta kart oyununun ihtiyacına göre yazılmıştı, yani her isteği ciddiye alan bir yapıydı.

Kule savunma tarafında bunun üzerine öncelik ve gruplama ekledik: aynı gruptaki sesler belirli bir pencerede birleşiyor, en yüksek öncelikli olan çalıyor. Bu davranış kart oyununda kapalı. Yani ortak modül artık iki farklı çalışma kipi taşıyor. Bunu bir başarısızlık saymıyorum, ama bedeli açık: modülü değiştiren kişi iki kipi de test etmek zorunda. Ses yöneticisinde yaptığımız her düzeltmenin, dokunmadığımız oyunda bir kez daha denenmesi gerekiyor.

Teknik borç nereye birikiyor?

Beklediğimiz yerde birikmedi. Kötü yazılmış paylaşılan kod bizi zorlamadı; fazla erken paylaşılmış kod zorladı. Bir modül iki projede de kullanılmaya başlandığı anda değiştirme maliyeti iki katına çıkıyor, çünkü her değişiklik iki oyunun regresyon testini gerektiriyor. Bu maliyet, kod olgunlaştıysa küçük; hâlâ şekil alıyorsa büyük.

Pratik sonuç: bir şeyi paylaşılan katmana taşımadan önce ikinci projede en az bir kez elle kopyalayıp kullanıyoruz. Kopya kod birkaç hafta iki yerde durur. O süre içinde iki kullanım birbirinden uzaklaşıyorsa, paylaşmamak doğru karardı demektir. Uzaklaşmıyorsa taşıma işi zaten kolay oluyor, çünkü artık iki gerçek kullanım örneği var, bir tahmin değil.

Karar verirken sorduğumuz üç soru

  1. Bu kod oyunun kuralını mı, kabuğunu mu ilgilendiriyor? Kural ise paylaşılmaz.
  2. İki projede aynı davranış mı, benzer davranış mı isteniyor? "Benzer" cevabı, ilerideki bayrak ve konfigürasyon yığınının habercisi.
  3. Bu modülü değiştirdiğimizde diğer oyunu test etmeye razı mıyız? Cevap hayırsa modül paylaşılmaya hazır değil.

Üçüncü soru en ayırt edici olanı. Kayıt sistemi ve yerelleştirme için cevabımız rahatlıkla evet: bu modüller aylardır büyük ölçüde sabit, değişiklik nadir ve testi mekanik. Ayarlar ekranı için hayırdı ve zaten ayrıldı.

Ölçemediğimiz kısım

Paylaşılan altyapının kazandırdığı zamanı kesin olarak ölçemiyoruz — küçük bir ekipte "paylaşmasaydık ne olurdu" senaryosunu koşturamıyorsunuz. Aynı belirsizlikle zorluk eğrisini telemetri olmadan okurken de karşılaşmıştık; oradaki yaklaşım burada da geçerli: sayı yoksa, tekrarlayan sürtünmeyi not alıp ona bakarsınız.

Bizim not defterimizdeki en sık tekrar eden sürtünme şuydu: "bu değişikliği yapmak için diğer oyunu da açmam gerekiyor." Bu cümlenin sıklığı, bir modülün doğru yerde olup olmadığına dair elimizdeki en dürüst göstergeydi.

Paylaş:

Yorumlar

İlk yorumu siz yapın.