Pong – Devlog 2: Oyun Geliştirme Başlasın!


Oyun serimizin ilk haftasını tamamladık. Ne yalan söyleyeyim, benim için beklediğimden çok daha iyi geçti! Arada ufak takılmalarımız olsa bile (bunları aşağıda paylaşacağım) bu haftaya 10 üzerinden 8 veririm. Bu yolculuğumuzu seninle paylaşmak istiyorum.

GDD

Aslında bu haftanın 10 puan almamasının en büyük sebebi bu! Çünkü ilk projeye başlarken planımızda GDD (Oyun tasarım belgesi)’nin ilk taslağını geçtiğimiz pazar günü yayınlamayı planlamıştım. Ancak bugüne nasip oldu. Aslında biraz iyi de oldu (yazar burada kendince bahaneler aramaya başlar …). Hah ne demiştim evet, bu sayede sana bu belgeyi biraz daha anlatabilirim. (Sen anladın onu artık iç sesimi burada susturuyorum!)

GDD normalde oyun fikri olan bir ekibin bir araya geldiğinde konuştuğu ve üzerine karar kıldığı Yusuf Miroğlu yasalarıdır. Hazırlanan ilk belgedir. Burada oyunun genel olarak, hangi platformda yayınlanacağı, genel özeti vs bir sürü kemik başlık netleşir. İlerleyen dönemlerde alınan tüm kararlarda bu belge bir referans teşkil eder. Ayrıca oyunu bir yayımcı veya yatırımcıya götürmek isterseniz onların da size soracağı ilk belgelerden biri bu olacaktır. Büyük AAA oyunlarının (internette pek çok örneğe ulaşabilirsiniz) GDD belgesi 200 sayfalara kadar ulaşırken ortalama bir indie yazılımcıda (tabi yazarsa) 20 – 30 kadar yer tutar. Bizim Pong oyunun ilk taslağı bile 12 sayfayı buldu. Bakmak istersen bu cümleye tıklayabilirsin.

Bu kadar sayfaya nasıl ulaşacağım diye hiç korkma. Zaten bir kere yazmaya başladığında ulaştığın sayfa sayısı seni çok şaşırtacaktır. Tabii ilk seferde açıklığa kavuşturamadığın pek çok konu olacak. O yüzden ilk taslakta sadece başlığını koyup, “sonra planlanacaktır” diye not alman yeterli. Aslında bu egzersiz bile oyun projesine bütüncül bir şekilde bakmana yardımcı olacaktır.

Biz, hazırladığımız dosyada ticari bir amaç hedeflemediğimiz için finansal değerlendirmeleri koymadık. Ayrıca bir yayıncıya gitme ya da yatırım almayı planlamadığımız için ekip üyelerinin öz geçmişini de eklemedik. Bunu özellikle belirtmek istedim. Çünkü bizim tasarım dokümanımız senin hedefine göre yeterli olmayabilir. Ama işin güzel yanı internette kısa bir gezintiyle çok daha güzel GDD taslaklarını bulabilirsin. Hatta biraz tembellik yapmak istiyorsan Double Coconut’ın paylaştığı (benim de çok beğendiğim) taslağa bu link ile de ulaşabilirsin.

Projeyi Yönetmek Yürütmek

Yönetmek kavramını, hep bir gizli üstünlük içerdiği için kullanmaktan çekinmişimdir. Özellikle hobi / gönüllü olarak yapılan bu gibi projelere çok yakıştıramıyorum. Konuya uzatmadan girmek adına bu muhabbeti burada kesiyorum.

Proje sahibinin en yoğun dönemi aslında proje planını hazırladığı dönemdir. Oyun ve geliştirici ekibin henüz toz ve bulutken büyük patlama (oyun geliştirmeye başlama) öncesi tüm atomları boşlukları düşünmen gerekmekte. Belki sana da faydası olur diye bu projenin planlama sürecini kısaca bahsetmek istiyorum:

  1. Önce planladığımız oyunun genel bir şablonunu çizdim.
    • Oyunun ana ekranın nasıl olacağı,
    • Ortalama oyun süresinin ne kadar olacağı,
    • Oyuncunun nasıl vakit geçireceği ve neler yapacağı,
    • Ne gibi mekanikleri olacağı,
    • Oyuncuları motive edecek hangi elementleri ekleyeceğimi,
    • Oyuna “benim” oyunum diyebilmek için hangi yenilikleri ekleyeceğimi belirledim.
  2. Oyunun genel bir özetini ekip arkadaşımla paylaştım ve fikrimi olgunlaştırdım
  3. Yapılacaklar listesini oluşturdum. Özellikle çok sevdiğim bir metod olarak bu listeleri basamaklandırdım.
  4. Hedeflediğim proje süresine göre her bir basamak için bir zaman çizelgesi belirledim.
  5. Yapılacakların ilk basamağını ekip üyeleri arasında paylaştım.
  6. Bunları Trello’da kart olarak ekledim.

Tolga ile ilk konuşmamızda projeyle ilgilenip ilgilenemeyeceğini sormuştum. Sağolsun o da büyük bir memnuniyetle kabul etti. İlk aramamızdan bir kaç gün sonrasında trello kartlarını ve miro diagramlarını paylaştım. Dün (3. görüşmemizde) ise ilk haftamızı değerlendirdik. Özellikle bu son toplantı benim için çok motive ediciydi. Geçen bir haftada oyun için kendi adına aldığı Udemy eğitimlerinden bahsetti (daha öncesinde Unreal Engine tecrübesi olduğu için Unity onun için biraz yeni bir alandı) ve yaptığı demoları gösterdi. Güzel bir proje için motive olmuş bir ekip arkadaşının ne kadar değerli bir şey olduğunu tecrübesi olan bilir.

Bundan sonraki süreçte her hafta için bir basamak daha ilerleyerek projeyi tamamlamayı umuyorum. Yine de olası gecikmeler için 2 haftalık bir sarkma haftası koyduk. Aşağıdaki ekran görüntüsünde durumu siz de görebilirsiniz:

6 haftalık bir projede 2 haftalık sarkma çok gözükebilir ama büyük oyunlarda bile görülen gecikmeleri iyi anlamak lazım: Oyun projeleri nadiren zamanında biter. Bunun olabilme ihtmali halley kuyruk yıldızının görülme ihtimaliyle bir. 


Kod Mimarisi

Hayatımda yalnızca bir tane profesyonel yazılım projesi yönettim yürüttüm. O yüzden kod mimarisi hazırlamak en güçlü yönüm değil. Fakat bunu daha çok kendim için her projede yapıyorum. Temel olarak şu şekilde gerçekleşiyor süreç:

  1. Daha öncesinden belirlediğim mekanikleri / oyun parçalarını gruplandırıyorum
  2. Bu grupları hangi scene (unity terimi olduğu için türkçeye çevirmedim) olacaklarına göre ayırıyorum.
  3. Bu grupları yönetecek genel “yönetici” sınıflarını ve bu kodları scene hiyerarşisinde kullanacak oyun objelerini tanımlıyorum.
  4. Sceneler arasında değişmeyecek olan sınıf ve datalar için bir işaret ekliyorum ve bunları hangi yolla sabitleyeceğime karar veriyorum.
  5. Son olarak yönetici sınıfları arasında nasıl bir iletişim olacağını tanımlıyorum.

Yukarıdaki resimdede gördüğün gibi “Game Manager” (nedense kodlamaları sadece ingilizce yapıyorum, türkçe ifadeler biraz garip geliyor – alışamadım bir türlü) tüm oyunun ana yönetimini yapan sınıf. Bir nevi CEO. Sınıflar arasındaki iletişimi yönetiyor, ilgili sınıfa gerekli temel bilgiyi iletiyor. Ayrıca sceneler arasında sabit olan tek sınıf. Normalde böyle bir sınıfı tanımlamak için Unity’de tavsiye edilen iki yöntem var:

  1. Sınıfı static olarak tanımlamak
  2. DontDestroyOnLoad metodunu kullanarak.

İlk seçenek (benim de seçtiğim yol) eğer Unity GameObject gibi spesifik objeleri taşımayacaksanız gayet elverişli bir yöntem. Burada en önemli dikkat edilmesi gereken nokta şu: Static olarak tanımlanan sınıflar, oyun açık olduğu süre boyunca hafızada yer kaplar. Eğer büyük bir sınıf veya çok data içeriyorsa yavaşlamalara neden olabilir.

İkinci seçenek ise Unity’e özel bir yöntem. Performans açısından çok sorun yaşayamayacağınız bir durum bu. Fakat özellikle sceneler arasında bağımlı objeler varsa bunu kullanırken dikkatli olmanızı tavsiye ederim. Metodu içeren sınıf / gameobject olabildiğince özerk olmalı. Ve diğer sceneler bundan veri çekerken sınıfın doğru yüklendiğinden emin olmalısınız.

Son olarak data özelinde sceneler arasında veri aktarımı için 3 seçenek daha mevcut:

  1. PlayerPrefs : Unity’e özel bir seçenek. Az miktarda basit verileri tutmak için ideal. Özellikle ayarlar seçeneğindeki veriler, oyuncu adı gibi veriler bu şekilde tutulabilir.
  2. ScriptableObject: Yine unity’e özel bir seçenek. Özellikle kompleks verileri tutmak adına harikalar yaratıyor. Performans anlamında aşağıdaki seçenekten daha hızlı olduğunu söyleyebilirim.
  3. XML/JSON/Binary veri serilizasyonu: Klasik, bildiğimiz anam babam yöntemi. Her türlü bilmeniz gerekir.

Player.cs

Yukarıdaki uyarıyı görenlerin şu itirazı yaptığını duyar gibiyim: “Arkadaş ağır kod içerir dedi ama ortada hiç kod yok! Proje de yürütmek de vs …” Eğer sen de onlardan biriysen hiç üzülme! Şimdi ilk kod paylaşımımızı yapıyorum, biraz da açıklama yaparak.

public class Player : MonoBehaviour 
{
     PlayerBase player;
     Rigidbody rb;
     GameObject playerObject;
     const float baseScale = 1.5f;
     const float baseBorder = 4f;
     public PlayerLine line;
     public PlayerType playerType;
     public float speed = 20f;
     public float currentScale;
     private float currentBorder;
     public Vector3 movement;
     void Start() 
    {
         playerObject = this.gameObject;
         rb = GetComponent<Rigidbody>();
         currentScale = baseScale;
         currentBorder = baseBorder;
     }
     void Update()
     {
         GetInput();
     }
     void FixedUpdate()
     {
         movement = movement * speed * Time.fixedDeltaTime;
         rb.MovePosition(new Vector3(transform.position.x, transform.position.y, Mathf.Clamp(transform.position.z + movement.z, -currentBorder, currentBorder)));
     }
     void GetInput()
     {
         if (transform.position.z <= currentBorder && transform.position.z >= -currentBorder)
         {
             if (player.type == PlayerType.player)
             {
                 if (line == PlayerLine.right)
                 {
                     movement = new Vector3(0f, 0f, Input.GetAxis("VerticalRight"));
                 }
                 else
                 {
                     movement = new Vector3(0f, 0f, Input.GetAxis("VerticalLeft"));
                 }
                 movement.Normalize();
             }
             else
             {
                  //Place AI Method here.
             }
         }
     }
     public void AssignPlayerData(PlayerBase playerData)
     {
         player = playerData;
          line = player.line;
         playerType = player.type;
         playerName = player.name;
     }
 }

Player sınıfı oyuncunun inputu aldığı ve gideceği yeri belirleyeceği ana sınıf. Yine ileride yapay zekayı kodladığımızda oyuncunun gideceği noktayı bu sınıfa iletecek.

Oyuncuya dair bütün veriyi PlayerBase sınıfında gruplandırdım. Burada niye struct kullanmadın diyenler olabilir. Açıkçası ciddi bir sebebi yok. Özellikle bu sınıfta kurduğum constructor (yapıcı) methodları diğer sınıflarda da kullanacağım için class daha uygun olur dedim. Bu sınıfı “Serializable” olarak tanımlamak unity editöründe içeriğini görülmesini sağlar. Aşağıda bu property’i görebilirsiniz.

[Serializable] 
public class PlayerBase
{
     public PlayerType type;
     public PlayerLine line;
     public string name;
     public int score;
     public PlayerBase(string nameData, PlayerLine lineData, PlayerType typeData)
     {
         this.type = typeData;
         this.line = lineData;
         this.name = nameData;
         score = 0;
     }
     public PlayerBase(string nameData, PlayerLine lineData)
     {
         this.type = PlayerType.player;
         this.line = lineData;
         this.name = nameData;
         score = 0;
     }
     public PlayerBase(PlayerType AIData, PlayerLine lineData)
     {
         this.type = AIData;
         this.line = lineData;
         this.name = GetAIName(type);
         score = 0;
     }
     private string GetAIName(PlayerType type)
     {
         string value = "";
         switch (type)
         {
             case PlayerType.aiEasy:
                 value =  "AI Easy";
                 break;
             case PlayerType.aiNormal:
                 value =  "AI Normal";
                 break;
             case PlayerType.aiHard:
                 value = "AI Hard";
                 break;
             default:
                 value = "AI";
                 break;
         }
         return value;
     }
}
public enum PlayerType
{
     player,
     aiEasy,
     aiNormal,
     aiHard
}
public enum PlayerLine 
{
     right,
     left
}

Bunun dışında oyuncuyu bulunduğu yere göre sağ ve sol (PlayerLine), yapay zeka olup olmamasına göre (PlayerType) olarak tanımladım. Ayrıca adı ve oyuncunun skorunu da burada tanımladım.

Player Line özellikle hangi inputun hangi oyuncuyu oynatacağını tanımlamakta. Ayrıca her input girişini Unity input manager (klasik input sistemindeki) ayarlarından tanımladım. Aşağıda player.cs’deki yerini görebilirsiniz:

if (player.type == PlayerType.player)
{
  if (line == PlayerLine.right)
  {
    movement = new Vector3(0f, 0f, Input.GetAxis("VerticalRight"));
  }
  else
  {
    movement = new Vector3(0f, 0f, Input.GetAxis("VerticalLeft"));
  }
}

Burada belki önemli başka bir noktayı paylaşmakta fayda var. İyi uygulama olarak bütün input verilerini void Update metodu içine koyarken fizik metodlarını FixedUpdate içine koymanız lazım. Bu biraz Unity’nin nasıl işlediği ile alakalı bir detay. Fakat burada önemli bir detay var: Klavyeden alınan input ile oyun fizik motorunun işlemesi arasında bir süre geçer. Bazı durumlarda bu istenmeyen hareketlere neden olabilir. Bunun için bazı bariyer kodlarına ihtiyacınız var:

... 
rb.MovePosition(new Vector3(transform.position.x, transform.position.y, Mathf.Clamp(transform.position.z + movement.z, -currentBorder, currentBorder)));
... 
if (transform.position.z <= currentBorder && transform.position.z >= -currentBorder)
...

Bu iki kod hem oyuncu sınırları aştığında input alımını durduruyor hem de hareketini kapatıyor. Bunlardan herhangi biri eksik olduğunda istemediğimiz bir titreşim / oyun sahası dışına çıkışla karşılaşabilirsiniz. Özellikle 12 float * fixedDeltaTime üstündeki hızlarda unity fiziği saçmalayabiliyor. Ki bu hız bizim oyuncularımız için çok az. Daha da yüksek bir hızı kullanmak istediğimiz için sadece Unity fizik motoruna güvenemedim.

Bunun dışındaki kodları da yavaş yavaş paylaşacağım. Şimdilik bu haftayı burada kapatalım. Bir sonraki yazımıza kadar güzel bir hafta geçirmeni diliyorum.

Get Just Another Game: Pong

Leave a comment

Log in with itch.io to leave a comment.