Pong – Devlog 5: Kodların Gücü Adına


Haftanın Özeti

Kodlamalarımızın azaldığı, tasarıma odaklandığımız haftalara geldik sonunda. Oyunun genel mekanikleri, ses yöneticisi ve leaderboardu da tamamladık. Projenin son 2 haftasına girdiğimiz bu dönemde;

  • Mekaniklerin ayarlarını tekrardan elden geçirmeye,
  • Yeni bölümleri tasarlamaya,
  • Kodlardaki hataları gözden geçirmeye ve açıklayıcı yorumlar eklemeye,
  • Görsel effekt ve sesleri eklemeye odaklanacağız.

İşin güzel yanı 2 bölümümüzü nerdeyse tamamladık (Bir klasik, bir de deniz bölümü). 2 bölüm daha eklemeyi planlıyoruz. Bunlar çöl ve neon bölümleri olacak. Her bölümün özgün olmasını ve farklı tecrübeler sunmasını istiyoruz. Genel olarak bu bölümler arasında farkları anlatmam gerekirse:

  • Klasik bölüm: Alışık olduğumuz pong tecrübesi burada olacak. Burada (ve diğer bölümlerde) ek olarak power upları kullanabileceksiniz.
  • Deniz bölümü: Kayalıkların arasındaki küçük bir gölde canı sıkkın iki kütüğün (evet oyuncular burada kütükleri hareket ettirecek) pong eğlencesine eşlik edeceksiniz. Burada suyun top hareketine etkisini hissedeceksiniz.
  • Çöl bölümü: Oyuncu modellerinin ne olacağına henüz karar veremedik (fikir ve önerilerinize açığız). Topun engelli (taşlar kaktüsler vs.) bir alanda oynanışını göreceksiniz.
  • Neon bölümü: Bu bölümü biraz Tron filmlerinden esinlendik. Bu bölüm diğerlerinden farklı olarak hareket eden bir engele sahip olacak. Topu burada oynuyor olacaksınız.

Açıkçası ilk oyuna başlarken 5 bölüm olmasını hedefliyorduk ama bu 4 bölümü belirlediğimizde 5. özgün bir bölüm aklımıza gelmedi. Belki ileride bir güncelleme yapabiliriz bilemedim şimdi.

Her zamanki gibi haftalık ayırdığımız süreyi sizinle paylaşmak istiyorum:

    
HaftaDokümantasyon (Blog, GDD vs)ToplantıGeliştirme (Ömer - Tolga)Toplam
Hafta 18 sa 30 dk - 8.5 sa
Hafta 22.5 sa 1 sa 2 sa - 30 dk 6 sa
Hafta 33 sa 2.5 sa 1.5 sa - 2.5 sa 9.5 sa
Hafta 43 sa 4.5 sa 11.5 sa - 2 sa 21 sa
Hafta 53 sa 3.5 sa 4.5 sa - 2.5 sa 13.5 sa
Toplam19.5 sa
12 sa
27 sa
58.5 sa
Süreleri yukarıya yuvarlayarak verdim.

Bu haftaki toplantıda leaderboard ve ses yöneticisine baktık. Unity'nin Auido Mixer ve diğer elementlerini gözden geçirdik. En çok başımızı ağrıtan olay oyun başladığında, ses ayarları sessizde olsa bile seslerin gelmesiydi. Sonunda bu problemi çözdük; tecrübemizi sizinle paylaşmak istiyorum: Audiomixer referansını ve başlatmasını SoundManager.cs'te yaparken ayarların bilgisini aldığımız scriptable objecti çağıran ve sesleri AudioMixere aktaran method MainGUIManager.cs'teydi. Ayrıca sliderlardan gelen veriyi mixere aktaran method da buradaydı. Oyun açıldığında ses ayarları bu mixere ulaşsa bile sesin olduğu AudioSource bunu kaale almayıp sesli bir şekilde çalıyordu.

İlk önce bu sorunun "Play On Awake" ayarıyla ilgili olduğunu düşündük ve onu iptal edip audio.Play() metoduyla çağırdık. Yine bir değişiklik olmadı. Proje ayarları --> "Script Execution Order"da SoundManager.cs ve MainGUIManager.cs'i öne almamız da bir şeyi değiştirmedi. Tüm ses dataların mixere aktarılması ve seslerin oynatılması ilgili kodların tamamını SoundManager'a aldığımızda sorun çözüldü. Buradan aldığımız ders şuydu: Peşpeşe işlemesini özellikle beklediğimiz ses resim gibi büyük dosyaların yönetimine ait tüm kod / metodları olabildiğince tek bir sınıfta tutmalı ve metod sekanslarının tamamlandığından emin olmamız lazım. Artık powerup konusuna geçebiliriz.

Gölgelerin Gücü Adına!

He-man izlediğim çocukluk zamanlarımda o pısırık kaplanın power up alarak muhteşem bir canavara dönüşmesi beni çok etkilerdi. Baksanıza hala aklımdaki ilk örnek o, ha bir de ay savaşçıları var malum (yazar burada derin bir sessizliğe düşer...). Hah ne demiştik, power uplar.. RPG oyunlarında da beni en çok çarpan şey buydu. Diablo II'nin güçleri günlerce rüyalarımdaydı.. Oradaki efektler, oyun rutinin dışına çıkabilmek...

Pong oyununda da kendim adına katabileceğim en önemli özelliğin bu olduğuna daha oyun fikri aklıma geldiğinde karar vermiştim. Nasıl olabileceğine dair bir kaç fikrim de vardı. Oyuncular başarılı bir kaç vuruş yaptıktan sonra belli bir şans oranında power up elde edebilecek (Eski Brick oyunlarındaki gibi) bu power uplar belli bir limitte stoklanabilecekti. Oyuncu istediği zaman da bu power up'ı kullanabilecekti. Aslında farkında olmadan power up yaşam döngüsünü çoktan kurmuştum bile.

Power Up Yaşam Döngüsü

Farkında olmadan diyorum çünkü bu farkındalığım internetteki rehberleri kurcalarken oluştu. Burada sizinle de muhteşem bir rehberi paylaşmak istiyorum: Raywenderlich. Her ne kadar bu rehberdeki kodlar benim projemde hiç işe yaramasa bile (gereğinden fazla kompleksti) yaklaşım metodu kendi kodumu oluştururken çok işime yaradı. Bu arada klasik bir RPG power up'ı için sizi fazlasıyla tatmin edecek bir rehberdir. Önermiş de olayım.

Yaşam döngüsünü kurmak, kullanacağım metod ve parametreleri belirlemede çok önemli bir rol oynadı. Her basamağı nerede kodlayacağım resmen gözümde canlanıyordu ki kodlama sürecim, bütün power upları implamente etmem ve bunları test etmem 40 dakikadan az zamanımı aldı. Şimdi benim örneğimle bu yaşam döngüsüne bir göz atalım:

Her 5 başarılı vuruşta oyuncunun %25 ihtimalle power up kazanması Oyuncu power up kazandığında bunu gösteren görsel efekt ve sesler Power up'ın slotlara eklenmesi Power up'ın oyuncu tarafından kullanılması Power up'ın gücünün ve görsel efektlerin uygulanması Power up'ın ve etkisinin sona ermesi
Ortaya Çıkma Her 5 başarılı vuruşta oyuncunun %25 ihtimalle power up kazanması
Dikkat Çekme Oyuncu power up kazandığında bunu gösteren görsel efekt ve sesler
Toplama Power up’ın slotlara eklenmesi
Kullanma Power up’ın oyuncu tarafından kullanılması
Etki Power up’ın gücünün ve görsel efektlerin uygulanması
Ölüm Power up’ın ve etkisinin sona ermesi
Power Up Yaşam Döngüsü

Buradan da anlayacağın gibi bu yaşam döngüsü benim oyunum için kurguladığım bir şey; muhtemelen ufak bir kaç değişiklik ile de sen de kendi oynunda kullanabilirsin. Ki yukarıda bahsettiğim rehber de bir benzerini kendi RPG'sinde kullanmıştı. Genel bir yaşam döngüsünü kurduktan sonra powerupların listesini belirledim. Toplamda 12'tane power up listeledim. Bunları da aşağıdaki gibi tek tek tanımladım:

Oyuncu Hit harder Oyuncu topa 2 kat daha sert vurur. Rakip Oyuncu Hit softer Rakip oyuncu topa 2 kat daha yumuşak vurur. Oyuncu Move faster Oyuncu 1.5 kat daha hızlı hareket eder. Rakip Oyuncu Move slower Rakip oyuncu 4 kat yavaş hareket eder. Top Size down - ball Top 0.5 kat küçülür. Top Size up - ball Top 2 katına büyür. Oyuncu Size up - player Oyuncu z ekseninde 1.5 kat uzar. Rakip Oyuncu Size down - player Rakip oyuncu z ekseninde 1.5 kat kısalır. Rakip Kalesi Size up - goal Rakip oyuncu kalesi z ekseninde 2 kat uzar. Oyuncu Kalesi Size down - goal Oyuncu kalesi z ekseninde 2 kat kısalır. Oyuncu Player immunity Oyuncu 10 sn boyunca gol yemez. Rakip Oyuncu Inverse input Rakip oyuncunun klavye inputları 10 sn boyunca tersine döner. Rakip eğer yapay zeka ise hata yapma olasılığı 1.5 kat artar.
AdıAdıEtkisi
OyuncuHit harderOyuncu topa 2 kat daha sert vurur.
Rakip OyuncuHit softerRakip oyuncu topa 2 kat daha yumuşak vurur.
OyuncuMove fasterOyuncu 1.5 kat daha hızlı hareket eder.
Rakip OyuncuMove slowerRakip oyuncu 4 kat yavaş hareket eder.
TopSize down – ballTop 0.5 kat küçülür.
TopSize up – ballTop 2 katına büyür.
OyuncuSize up – playerOyuncu z ekseninde 1.5 kat uzar.
Rakip OyuncuSize down – playerRakip oyuncu z ekseninde 1.5 kat kısalır.
Rakip KalesiSize up – goalRakip oyuncu kalesi z ekseninde 2 kat uzar.
Oyuncu KalesiSize down – goalOyuncu kalesi z ekseninde 2 kat kısalır.
OyuncuPlayer immunityOyuncu 10 sn boyunca gol yemez.
Rakip OyuncuInverse inputRakip oyuncunun klavye inputları 10 sn boyunca tersine döner. Rakip eğer yapay zeka ise hata yapma olasılığı 1.5 kat artar.
Power Up Listesi

Artık power uplarımız ve yaşam listesi hazır olduğuna göre kodlamaya başlayabiliriz:

using System;
using System.Collections.Generic; using UnityEngine;
public class PowerUpBase : MonoBehaviour
 {
     public string powerUpName;
     public string description;
     public Sprite sprite;
     public Transform activePlayer;
     public Transform otherPlayer;
     public Transform ball;
     public Transform specialEffect;
     public float powerUpTime;
     public float specialEffectTime;
     public virtual void ActivatePowerUp(PowerUpIndex index)
     {
         Invoke("ResetPowerUp", powerUpTime);
     }
     public virtual void ResetPowerUp()
     {
     }
     public virtual void ActivateSpecialEffect(PowerUpIndex index)
     {
         Invoke("DeactivateSpecialEffect", specialEffectTime);
     }
     public virtual void DeactivateSpecialEffect()
     {
         specialEffect.gameObject.SetActive(false);
     }
 }

İlk önce tüm power uplarımızın kalıtım alacağı ana sınıfımızı kodladık: PowerUpBase.cs. Burda abstract veya interface kullanmıyor olmam sadece bir tercih. Malum bir çocuk sınıf sadece tek bir ana sınıftan kalıtım alacağı için bu sınıfı da MonoBehaviour'dan kalıtım aldırdık. Sahne içinde bu power upların tamamını ManagerGO gameobjesinin altına ekledik - bir çocuk gameobjesi olarak. Yaşam döngüsünün 3 parçasını da buraya metod olarak ekledik: Dikkat çekme, Etki ve Ölüm. Ölüm döngüsü hem özel efekt için hem de power up için geçerli. Bu metodların hepsinin "virtual" olduğunu vurgulamak istiyorum. Çünkü her power up burada kendi metodlarını / etkilerini gösterecek. Bu sınıfta eklemek istediğim son bir şey daha var: Her power up kullanıldığında ("ActivatePowerUp( index)") kendi imha metodunu da otomatik olarak tetikliyor. Bu da daha sonrasında ölüm döngüsünü implamente etme derdinden beni kurtarıyor.

Invoke("ResetPowerUp", powerUpTime);

Gelelim diğer döngülerin implementasyonuna...

Doğum

     void OnCollisionEnter(Collision other)
     {
         ...
              if (other.transform.GetComponent<PlayerManager>().line == PlayerLine.right)
             {
                 rb.velocity = new Vector3(10.0f, 0.0f, (5.0f * zValue)) * other.transform.GetComponent<PlayerManager>().hitModifier;
                 if (GameManager.allowPowerUps)
                 {
                     RollPowerUp(PlayerLine.right);
                 }
             }
             else
             {
                 rb.velocity = new Vector3(-10.0f, 0.0f, (5.0f * zValue)) * other.transform.GetComponent<PlayerManager>().hitModifier;
                 if (GameManager.allowPowerUps)
                 {
                     RollPowerUp(PlayerLine.left);
                 }
             }
            ...
     }
     void RollPowerUp(PlayerLine line)
     {
         if (line == PlayerLine.left)
         {
             lPlayerPUCount++;
             if (lPlayerPUCount > maxPUCount)
             {
                 lPlayerPUCount = 0;
                 pm.InitiatePowerUp(line);
             }
         }
         else
         {
             rPlayerPUCount++;
             if (rPlayerPUCount > maxPUCount)
             {
                 rPlayerPUCount = 0;
                 pm.InitiatePowerUp(line);
             }
         }
      }

Yukarıda kodlarının bir kısmını verdiğim BallManager her oyuncu topa çarptığında -eğer oyun içinde power up'a izin verilmişse ( if (GameManager.allowPowerUps) )- RollPowerUp() metodunu çağırıyor. Bu metod ise öncelikle oyuncunun _PUCount'unu bir arttırıyor. Maksimum sayıya ulaştığında ise, bizim oyunumuzda bu sayı 5, PowerUpManager sınıfında InitiatePowerUp() metodunu çağırıyor. Sonrasında _PUCount'u sıfırlıyor.

PowerUpManager sınıfında ise %25 ihtimalle oyuncu power up hakkı kazanmış oluyor. Power up hakkı kazanan oyuncu ise aktif power uplardan birini yine random bir şekilde kazanmış oluyor. Metodu aşağıda paylaşıyorum:

     public void InitiatePowerUp(PlayerLine line)
     {
         float randomValue = UnityEngine.Random.Range(0f, 2.5f);
         if (randomValue < 1)
         {
             int value = (int)Math.Round((decimal)UnityEngine.Random.Range(0, powerUpList.Count), MidpointRounding.ToEven);
             PowerUpBase powerUp = powerUpList[value];
             AddPowerUp(line, powerUp);
         }
      }

Toplama

Artık oyuncunun hangi Power up'ı kazandığını biliyoruz. Şimdi sıra stoklamada. Stoklama işlemi için ilk önce nereye stoklayacağımızı belirlememiz gerekiyor. Aslında bunu pek çok şekilde yapabiliriz ama oyunda toplamda 4 power up alanı olacağından enum bizim için fazlasıyla yeterli.

 public enum PowerUpIndex
 {
     leftFirst,
     leftSecond,
     rightFirst,
     rightSecond
 }

Bu slotları PowerUpBase'de tanımladık: Her oyuncunun iki alanı mevcut. İsimlendirmesini de buna uygun bir şekilde yaptık. Bu enum listesine uygun bir şekilde, PowerUpManager sınıfında benzer adlarda (rightFirstPowerUp gibi) PowerUpBase referansları oluşturduk. Sıra geldi, oyuncunun kazandığı power upları enum değerlerine göre ilgili paramatrelere yerleştirmeye. Bunu da AddPowerUp() metodu ile sağladık:

     void AddPowerUp(PlayerLine line, PowerUpBase powerUp)
     {
         if (line == PlayerLine.left)
         {
             if (leftFirstPowerUp == null)
             {
                 leftFirstPowerUp = powerUp;
                 guiManager.AddPowerUpImage(PowerUpIndex.leftFirst, powerUp.sprite);
                 return;
             }
             else if (leftSecondPowerUp == null)
             {
                 leftSecondPowerUp = powerUp;
                 guiManager.AddPowerUpImage(PowerUpIndex.leftSecond, powerUp.sprite);
                 return;
              }
             else
             {
                 Debug.Log("PowerUp slots are full for left player!");
                 return;
             }
         }
         else
         {
             if (rightFirstPowerUp == null)
             {
                 rightFirstPowerUp = powerUp;
                 guiManager.AddPowerUpImage(PowerUpIndex.rightFirst, powerUp.sprite);
                 return;
             }
             else if (rightSecondPowerUp == null)
             {
                 rightSecondPowerUp = powerUp;
                 guiManager.AddPowerUpImage(PowerUpIndex.rightSecond, powerUp.sprite);
                 return;
             }
             else
             {
                 Debug.Log("PowerUp slots are full for right player!");
                 return;
             }
         }
     }

Yaptığımız şey aslında çok basit. Her bir PowerUpIndex'e karşılık benzer addaki PowerUpBase parametresinin boş olup olmamasına göre de oyuncunun hak kazandığı power up'ı ekledik. Eğer oyuncuya ait tüm slotlar dolu ise bir konsol bilgilendirmesi yapıyor. GUI'ye ikonun eklenmesi de bu metodda gerçekleşiyor.

Kullanma

Son olarak ele alacağımız kısım ise power up'ın kullanılması / aktiflenmesi. Doğal olarak ilk önce bir input almamız gerekiyor. PlayerManager'ın update kısmında bu inputu almak için ActivatePowerUp() metodunu çağırmamız yeterli olacaktır.

     void ActivatePowerUp()
     {
         if (player.type == PlayerType.player)
         {
             if (Input.GetKeyDown(KeyCode.A))
             {
                 pm.ActivatePowerUp(PowerUpIndex.leftFirst);
             }
             if (Input.GetKeyDown(KeyCode.D))
             {
                 pm.ActivatePowerUp(PowerUpIndex.leftSecond);
             }
             if (Input.GetKeyDown(KeyCode.LeftArrow))
             {
                 pm.ActivatePowerUp(PowerUpIndex.rightFirst);
             }
             if (Input.GetKeyDown(KeyCode.RightArrow))
             {
                 pm.ActivatePowerUp(PowerUpIndex.rightSecond);
             }
         }
     } 

Yukarıda da gördüğün gibi her input ile PowerUpManager (pm)'da ActivatePowerUp() metodunu çağırıyoruz. (Evet biliyorum metod isimleri aynı) Burada hangi slotu kullandığımızı da yine PowerUpIndex ile belirliyoruz. Bundan sonrası basit bir switch-case döngüsüyle çok rahat yönetilebilir ki biz de PowerUpManager.ActivatePowerUp() metoduyla onu yaptık:

     public void ActivatePowerUp(PowerUpIndex index)
     {
         switch (index)
         {
             case PowerUpIndex.leftFirst:
                 if (leftFirstPowerUp != null)
                 {
                     leftFirstPowerUp.ActivatePowerUp(index);
                     guiManager.RemovePowerUpImage(PowerUpIndex.leftFirst);
                     leftFirstPowerUp = null;
                 }
                 break;
             case PowerUpIndex.leftSecond:
                 if (leftSecondPowerUp != null)
                 {
                     leftSecondPowerUp.ActivatePowerUp(index);
                     guiManager.RemovePowerUpImage(PowerUpIndex.leftSecond);
                     leftSecondPowerUp = null;
                 }
                 break;
             case PowerUpIndex.rightFirst:
                 if (rightFirstPowerUp != null)
                 {
                     rightFirstPowerUp.ActivatePowerUp(index);
                     guiManager.RemovePowerUpImage(PowerUpIndex.rightFirst);
                     rightFirstPowerUp = null;
                 }
                 break;
             case PowerUpIndex.rightSecond:
                 if (rightSecondPowerUp != null)
                 {
                     rightSecondPowerUp.ActivatePowerUp(index);
                     guiManager.RemovePowerUpImage(PowerUpIndex.rightSecond);
                     rightSecondPowerUp = null;
                 }
                 break;
             default:
                 Debug.Log("Image index is not defined here");
                 break;
         }
     }

Doğal olarak kullanım sonunda hem ilgili power up slotunu hemde GUI'deki alanı boşalttık.

Dikkat Çekme, Etki ve Ölüm döngülerini yukarıda biraz anlattık. Bunlar her power up'ta farklı bir şekilde kodlanacaktır ama sınıf ve genel metodları aynı kalacaktır. Override yaparak bu metodları herbir power up için özelleştirebiliriz. Detaylarını tek tek anlatmak bu haftaki yazıyı çok uzatacağından burada bahsetmeyeceğim. Ama onları da tüm kodları github'a yüklediğimizde görebileceksin. Haftaya yapay zekayı konuşma sözüyle bir veda girişi yapmış olayım. Bir sonraki yazımıza kadar muhteşem bir hafta geçirmen temennilerimle…

Get Just Another Game: Pong

Leave a comment

Log in with itch.io to leave a comment.