Pong - Devlog 4: Seken Top


Haftanın Özeti

Yoğun kodlamayla geçen bir haftayı daha devirdik. Bu hafta özellikle programın önünde giderek top fiziğini oturtmanın yanında; power upların implementasyonunu tamamladık ilk görsel geliştirmelerimizi yaptık ana menu ekranı ile oyun ekranını bağladık. Aslında oyununun kabasının %85’ini tamamladık. Ne mi eksik kaldı:

  1. Yapay zeka power upları kullanamıyor henüz.
  2. Ses yöneticisi ve seslerin implamantasyonun yarısı kaldı.
  3. Leaderboard henüz tam olarak implamente edilmedi.
  4. Oyun ekranında gol atıldığında vs bilgi veren pop up’ı tamamlamadık.
  5. Görsel efektler ve sesler yok!

Özellikle 23 Nisan tatilinin olması benim çok işime geldi. 2 günde (Cuma, Cumartesi) doya doya kodlama yapabildim. Onun dışında TextMeshPro ve GUI tasarımına odaklandığımız toplantı da artık akşam yemeği saati geldi bu haftalık tamam demeseydik bir 6 – 7 saat sürebilirdi. Her zamanki gibi haftalık ayırdığımız süreyi tabloyla size 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
Toplam16.5 sa
8.5 sa
20 sa
45 sa
Süreleri yukarıya yuvarlayarak verdim.

Geçen hafta söz verdiğim gibi top fiziğine girmeden önce toplantıda fark ettiğimiz şeylerden birini sizinle paylaşmak istiyorum. Özellikle “Vertical Layout Group” ve/ya “Horizontal Layout Group”u kullanmak isteyen arkadaşların bu noktaya dikkat etmesi GUI’de taşan elementleri düzeltirken işlerini kolaylaştıracaktır: RectTransformdaki scale ayarları!

Scale değerlerinizin 1 olduğundan emin olun. Çeşitli nedenlere bu ayarlar değişmiş olabilir ve bunun sonucunda garip boyutta GUI elementleri görebilirsiniz. Bİz gördük de oradan biliyoruz!

Topun Fiziğini Anlamak

Oyuna başladığımızda aslında en kolay halledeceğimiz konun bu olduğunu düşünüyordum. Burada 3 – 4 saat vakit harcadığım düşünülürse ne kadar da yanıldığım kabak gibi ortaya çıkacaktır. Önce yaşadığımız sorunları, sonrasında bunu çözmek için neler yaptığımızı konuşalım.

Sorunlarımız:

  1. Top özellikle yüksek hızlarda oyun alanının dışına çıkabiliyordu ve oyuncunun içine sıkışabiliyordu.
  2. Top bazen yolun ortasındayken bir anda açısını değiştirip başka yöne hatta tam tersine gidebiliyordu.
  3. Top karşı oyuncuya gitmeden önce öyle bir yavaşlıyordu ki bazen oyuncunun önünde durabiliyordu.

Topun yüksek hızlarda collision detection (çarpışma algılama)’yı doğru yapamaması aslında yeni bir konu değil. Burada Unity’den kaynaklanan kısıtlamalar olduğu gibi, fizik yaşam döngüsünü doğru anlamadığımız için bizim de hatamız oldu. Bunu çözmek için Unity dökümantasyonunu okudum, diğer insanların kodlarına baktım ve internetteki diğer blogları inceledim. Eskisine göre biraz daha iyi çalışan bir sistem kurmakla birlikte hala mükemmel bir çözüm bulamadığımı itiraf ederek işe başlayayım.

İlk önce rigidbody deki ayarlara odaklandım. İstediğimi karşılayan en yakın seçenekler: Interpolate “Interpolate” ve Collision Detection “Continuous Speculative”di. Bu da yeterli olmayacınca Project Settings > Time > Fixed TimeStep’i küçültmeye başladım. İşin ilginç yanı 0.02’nin altındaki değerlerde (En son 0.005 değerinde karar kıldım) yine saçma sapan top hareketleriyle karşılaşıyordum. Bu durum ise Interpolate değerini “None”a geri döndürünce düzeldi. Bu konu ile alakalı çok güzel bir blog serisi buldum. Konuya tekrar geri döneceğim.

Son Haliyle Topun Rigidbody Ayarları

Düzeldi dediğime bakmayın özellikle çok yüksek hızlarda hala top oyun alanı dışına çıkabiliyordu. O yüzden player inputtaki uyguladığım taktiği buraya da uyarladım. Top, oyun alanının sınırlarına gelince transform değerlerindeki değişikliği kitledim kısaca. İşte kodu:

     void Update()
     {
         ...
         StayInbounds();
         ...
      }
     void StayInbounds()
     {
         transform.position = new Vector3(
             Mathf.Clamp(transform.position.x, leftBound, rightBound),
             transform.position.y,
             Mathf.Clamp(transform.position.z, bottomBound, topbound));
     }

Bu kodla oyun alanın sınırlarını ball manager’a aktarıp bu sınırları her update’de kontrol ettim. Performans açısından zaten çok az oyun objesi olduğu için hiç bir sorunla karşılaşmadım.

Yukarıdaki düzenlemelerle top oyun alanını hiç çıkmamayı başardı fakat bazı durumlarda (sebebini hala bulamadım) oyuncuya top çarptığında sabit kalıyordu. Ve bir daha hareket etmiyordu. Bunun açıkçası çok sık olmaması güzel bir şey ama ne yaptıysam güzel bir çözüm bulamadım. Durum böyle olunca da oyunun kilitlenmemesi için topun pozisyonunu resetleyen bir input tuşu eklemekten başka bir çarem kalmadı ve ben de tam bunu yaptım:

     void Update()
     {
         ...
         if (Input.GetKeyDown(KeyCode.R))
         {
             StopRound();
         }
     }
     public void StopRound()
     {
         tr.enabled = false;
         if (!GameManager.isLevelFinished)
         {
             rb.velocity = Vector3.zero;
             rb.isKinematic = true;
             Invoke("ResetBallPosition", 0.5f);
             Invoke("PitchBall", 2f);
         }
     }
     void ResetBallPosition()
     {
         transform.position = Vector3.zero;
     }
 
     public void PitchBall()
     {
         rb.isKinematic = false;
         float rand = Random.Range(0f, 2f);
         float randBall = Random.Range(0f, 2f);
         float randZValue;
         if (randBall < 1)
         {
             randZValue = Random.Range(-15f, -5f);
         }
         else
         {
             randZValue = Random.Range(5f, 15f);
         }
         if (rand < 1)
         {
             rb.velocity = new Vector3(10.0f, 0.0f, randZValue) * startermodifier;
         }
         else
         {
             rb.velocity = new Vector3(-10.0f, 0.0f, randZValue) * startermodifier;
         }
     }

Stop Round ve diğer metodları zaten top her gol olduğunda veya, kalenin olduğu sınıra çarptığında uyguluyordu sınıf. Buna ek olarak oyun içerisinde bu metodu bir tuşla çağırabilme şansı verdim oyunculara. Buradaki metodları ileride bir daha ele alacağız ama şimdiden açıklayalım:

StopRound: İlk önce topa eklediğimiz bir trail renderi (görsel efekt olması için ekledik)  kapatarak işe başlıyor. Bu sayede topun pozisyonu değişirken kalıntı bir trail görmüyoruz. Sonrasında oyunun bitip bitmediğini Game Manager ile kontrol ediyor ve akabinde oyun bitmediyse topumuzu durduruyor ve topumuza etki eden bütün fiziksel vektörleri iptal ediyoruz. Buradaki isKinematic boolean’ı tamamen bu fizik vektörleri iptal etmek için var. (Aşağıda biraz daha bahsedeceğiz bundan…) Sonrasında ilk önce topun pozisyonunu değiştiriyoruz ve ardından topa ilk vuruşu yapıyoruz. Buradaki Invoke metoduna eklediğimiz süre ise tamamen oyuncunun durumdan haberdar olması ve yeni tura hazırlanmasına vakit vermek için. Eğer direk diğer metodları çağırsaydık kafa karışıklığına neden olabilirdi. Invoke metodu ile önce top olduğu yerde bir duruyor, yarım saniye sonra 0,0,0 noktasına ışınlanıyor ve bundan 1.5 saniye sonra da hareket etmeye başlıyor.

ResetBallPosition: Gayet adından da anlaşılacağı gibi topu 0,0,0 noktasına geri ışınlıyor. Hepsi bu!

PitchBall: Bu metodda iki tane random değer tespit ediyoruz. Buradaki ilk değer topun hangi yöne gideceğini belirlemesi için ikincisi ise her seferinde farklı bir açı ile gitmesi için hazırlandı. En son belirlenen yön vektörünü rb.velocity parametresine aktarıyoruz. Bu velocity meselesini sonra ele alacağız. Ayrıca hızı daha iyi modifiye edebilmek için de starterModifier olarak oluşturduğumuz float bir değerle çarpıyoruz. Yaptığımız bir sürü testten sonra bizim için en uygun rakamları bulup gayet statik bir şekilde koda ekledik. Bunun sebebi ise bunları ileride de değiştirmeyi planlamadığımız için. starterModifier kodu değişik levellarda başka değerler alacağı için bunu public olarak tanımladık. Atlamadan da söyleyelim; fizik vektörlerin topu tekrar etkilenmesi için de isKinematic değerini false yaptık burada.

İkinci olarak topun özellikle oyunun başında saçma bir şekilde ani yön değiştirmesine bakalım. Aslında bunu nasıl çözdüğümüzü şimdiye çoktan anlamışsındır. Ama kısaca açıklayayım: isKinematic parametresi ile. Önce sorunu açıklarsam buna neden ihtiyacımız olduğunu daha iyi anlayabilirsin. Sorun biz turu bitirip oyunu 0,0,0 noktasına ışınladığımızda topun hala üzerinde force’ların etkisiyle hareket etmesi. Normalde duvara veya kaleye çarptığı için görüntüde duran top sıfır noktasına geri geldiğinde hareket etmeye başlıyor. Çünkü önünde engel yok. 2. saniyenin sonunda gelen PitchBall metoduyla top bu sefer random bir yöne doğru ilerlemeye başlıyor. İşte bu ani yön değişikliğe neden olan şey de bu! Bunu isKinematic değerlerini true-false yaparak çözdük.

Gelelim son sıkıntımıza yani yavaşlayan topa! Oyuna ilk başlarken doğal olarak unity’nin fizik motoruna olabildiğince az müdahale etmek istedim. Sebebi basitti, ağır yükü unity fizik motoruna verirsem ben de kolayca “BallManager”ı kodlayıp diğer konulara geçebilecektim. Bu şu demekti rb.velocity değerlerine hiç karışmayıp rb.AddForce ile topu hareket ettirmek!

Önce niçin bu yolu tercih ettiğimi kısaca açıklamak istiyorum. Velocity değeri rigibody’nin nihai anlık hareket değeridir. Tüm fizik hesaplamaların sonunda bu ve Speed parametresi çıkar. Eğer direk bu parametreye müdahale edersen önceki bütün fizik güçlerini susturmuş olursun (yer çekimi, sürtünme kuvveti, iten bir obje vs.) Ben de doğal bir top hareketi istediğim için rb.AddForce metodunu kullanmaya karar verdim Aslında istediğim doğallığı sağladı. Top doğal bir şekilde hızlandı, doğal bir şekilde ilerledi ve doğal bir şekilde geldi oyuncunun önünde durdu. Niye çünkü tek seferlik gücün etkisi bitmişti. Gücü arttırdığımda direk paslaşmalarda yine rahattım ama duvara sekerek giden toplar yine yolun yarısında yavaşlıyordu. Daha da arttırdığımda ise bu sefer roket gibi gidiyor ve kontrolü zor oynanması zevksiz bir topa dönüşüyordu. Ama Allah var gayet doğaldı!

Doğallıktaki bu ısrarımı hala devam ettirdiğim için bu sefer dedim ki alttan alta hep ConstantForce uygulayayım. Belli bir limite geldiğinde durdurayım. Bu sefer sürekli hızlanan bir top ile karşılaştım. Constant Force‘u kontrol etmek de oyun performansını %30 etkiledi. (Yukarıda time stepi 4 kat daha hızlı yaptığımda bile performans kaybım % 10’lardaydı) İşin doğallığı zaten kalmadı. Özellikle 8 – 9 kere oyuncular arasında giden top çok hızlanıyordu. Her seferinde gerekli Constant Force vektörünü hesaplamak ve bunu kodlamak da beni bezdirdi açıkçası. Çünkü başta uygulasan garip hızlı bir top oluyor ve oyuncuya duvara saplı kalabiliyor, sonradan uygulasan “Sweet Spot” yani tam bamteli değerlerini (süre ve kuvvet gücü) tespit etmekte zorlanıyorum. Kısacası bundan da vazgeçtim. (Bu arada zaten drag değerim tüm objelerde 0. Olurda aklına gelirse burada söylemiş olayım. Yani sürtünme zaten yok ortada.)

Doğallığı sağlamanın zor olduğunu fark edince tam bir oyun geliştiricisi modunda (herhalde tüm proje boyunca kendimi game developer olarak en çok hissettiğim an budur) bu ısrarımdan vazgeçtim ve kuzu kuzu velocity’e yöneldim. Açıkçası minimal velocity alt sınırı da konabilirdi şuan bu blogu yazarken bir kez daha fark ediyorum ama zaten burada çok vakit kaybettiğim için bir an önce çözmek istedim ve bu seçeneği direk eledim o an. Dedim ya tam oyun geliştiricisi modundayım: Oyun dediğin simulatif bir yalan / ilüzyon değil midir arkadaş! Ne yer çekimi ne doğallığı! Hıh!

Oyunda duvara çarparken aldığı vektörel gücü korumak adına kısıtlı bölgelerde velocitye direk olarak müdahale ettim. Tur başlarken ki müdahaleyi zaten yukarda gördün. Ben oyuncu, kale ve kale tarafındaki sınırların müdahalesini de buraya ekleyeyim:

     void OnCollisionEnter(Collision other)
     {
         if (other.transform.parent.tag == "EndBorder")
         {
             StopRound();
         }
         else if (other.transform.parent.tag == "Goal")
         {
             StopRound();
             SetScore(other.transform.name);
         }
         else if (other.transform.parent.tag == "Player")
         {
             other.transform.GetChild(0).position = other.GetContact(0).point;
             float zValue = other.transform.GetComponent<PlayerManager>().SetAngle(other.transform.GetChild(0).localPosition);
             other.transform.GetComponent<PlayerManager>().isBallContacted = true;
             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);
                 }
             }
             tr.enabled = true;
             Invoke("DisableTrail", 0.6f);
         }
     }

EndBorder: Kale tarafındaki oyun sınırlarına atadığım tag buydu. Doğal olarak o turu bitirip yeni turu başlatıyor başka bir olayı yok.

Goal: Yeni turu başlatmak ile birlikte karşısındaki oyuncuya puan ekliyor. Bu kodu da aşağıda paylaşayım:

     void SetScore(string name)
     {
         if (name == "Goal_Left")
         {
             if (!lm.isLeftPlayerImmune)
             {
                 GameManager.UpdateScore(0, 1);
                 lm.onScored();
             }
         }
         else
         {
             if (!lm.isRightPlayerImmune)
             {
                 GameManager.UpdateScore(1, 0);
                 lm.onScored();
             }
         }
     }

Kısaca oyun objesinin adına göre karşıdaki oyuncuya skor ekliyor. Bu skorlar iki yerde tutuluyor bir GameManager sınıfında (Oyundaki statik tek sınıf bu verileri sceneler arasında taşıyacak. Ayrıca leaderboard’a da skorları o verecek.) ve LevelManager‘da (lm olarak referans verdim burada). Level manager sadece bu scene’de skoru tutuyor. Ayrıca sağladığı Delegate – Event ile (onScored()) GUI’ye güncellemesi için haber gönderiyor. Burada ayrıca bir power up implementasyonunu görüyoruz: lm.isRightPlayerImmune. Bu ise golü kaydetmeden önce oyuncunun gole immün (doktorluğum konuştu farkındayım) olup olmadığına bakıyor. Haftaya bu konuyu inceleyeceğiz.

Player: Dananın kuyruğunu birlikte koparalım. Oyuncuyu anlatırken söylemiştim. Topa vurduğu yere göre açı vermesini istiyordum. İşte bunun için oyuncunun içine bir GameObjesi koydum. Sadece transform bileşeni olan bir obje. Top oyuncuya çarptığı noktaya biz bu objeyi ışınlıyoruz. Sonra merkeze göre uzaklığını hesaplayıp (lokal pozisyon değeri – oyuncu büyüse bile burası sabit kalıyor. Yey!) bu uzaklıktan PlayerManager.SetAngle() metoduyla vermek istediğimiz açıyı belirliyoruz. Burada bir açıklama yapmak için es vermem gerek!

Normalde ilk PlayerManager.SetAngle() metodunu yazdığımda hala AddForce() metodunu kullanıyordum. O zamanlar açıya göre tanjant (trigonometri’den hatırlarsınız) değerini alıp buradan z eksenindeki değerini buluyordum. Sonra niye bu kadar kastım ki diye düşünüp direk z eksenindeki değeri daha doğrusu modifiye edecek katsayıyı almaya başladım. Ama metodun ismi geçmişe selam çakmak adına Set Angle olarak kaldı. Özetle artık sadece z değerlerini bir katsayı ile çarparak açı veriyorum. Bunun implemantasyon kodunu aşağıda bir daha paylaşalım:

rb.velocity = new Vector3(10.0f, 0.0f, (5.0f * zValue)) * other.transform.GetComponent<PlayerManager>().hitModifier;

Burada yine sabit değerler kullanıp bunların açısını zValue ile nihai hızını ise oyuncun hitModifier değeri ile yükseltim. Bir power up’ı (daha hızlı veya yavaş topa vurabilme özelliğini) da bu şekilde eklemiş oldum. Ayrıca power up elde edebilme şansını da oyuncuların başarılı top vuruşlarına bağladım. Böylece her 5 başarılı topa vurmada oyuncunun power up kazanma şansı var. Tabi oyunda power up’a izin varsa (bkz. GameManager.allowPowerUps) . İlgili metodu da aşağıda eklemiş olalım (Buradaki pm PowerUp Manager’ı refere ediyor):

     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);
             }
         }
      }

Burada değinmediğim son bir yer kaldı o da other.transform.GetComponent<PlayerManager>().isBallContacted = true; Bunu daha çok yapay zekayı kontrol etmek için ekledim. İleride yapay zekayı anlattığımda buraya değineceğim ama kısaca özetlemek gerekirse: Yapay zeka top kendinden çıktıktan sonra kalenin ortasına geri dönüyor. Top karşı oyuncuya değdiği anda ise topun nereye gideceğini hesaplayıp oraya gitmeye çalışıyor. Bunu OnCollisionExit metoduna koymadım çünkü çok da fark yaratmayacaktı. Biraz üşenmiş olabilirim, bilemedim.

Genel olarak Ball Manager’ı ve top fiziğini konuşmuş olduk. Sonuçta yarı doğal bir top fiziğini elde ettik. Yarı doğal diyorum çünkü oyuncu ile çarpışma anında ve başlangıçta direk velocity’i düzenliyorum. Onun dışında tamamen Unity fizik simulasyonlarına tabi. Çok az bir miktarda da olsa top yavaşlamaya devam ediyor. Ki bu durumdan gayet memnun kaldım. Her zamanki gibi kodun tamamı:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BallManager : MonoBehaviour
{
     Rigidbody rb;
     LevelManager lm;
     PowerUpManager pm;
     TrailRenderer tr;
     public PlayerLine movementDirection;
     public Vector3 sideTarget;
     public Plane rightPlane, leftPlane;
     public float startermodifier = 1.5f;
     const float leftBound = -9.55f, topbound = 4.55f, rightBound = 9.55f, bottomBound = -4.55f;
     const int maxPUCount = 4;
     int lPlayerPUCount = 0, rPlayerPUCount = 0;
     void Start()
     {
         rb = transform.GetComponent<Rigidbody>();
         tr = transform.GetComponent<TrailRenderer>();
         tr.enabled = false;
         rightPlane = new Plane(Vector3.left, new Vector3(-12f, 0f, 0f));
         leftPlane = new Plane(Vector3.right, new Vector3(12f, 0f, 0f));
         lm = GameObject.FindGameObjectWithTag("Manager").GetComponent<LevelManager>();
         pm = GameObject.FindGameObjectWithTag("Manager").GetComponent<PowerUpManager>();
         PitchBall();
     }
     void Update()
     {
         GetTargetInfo();
         StayInbounds();
         if (Input.GetKeyDown(KeyCode.R))
         {
             StopRound();
         }
     }
     void OnCollisionEnter(Collision other)
     {
         if (other.transform.parent.tag == "EndBorder")
         {
             StopRound();
         }
         else if (other.transform.parent.tag == "Goal")
         {
             StopRound();
             SetScore(other.transform.name);
         }
         else if (other.transform.parent.tag == "Player")
         {
             other.transform.GetChild(0).position = other.GetContact(0).point;
             float zValue = other.transform.GetComponent<PlayerManager>().SetAngle(other.transform.GetChild(0).localPosition);
             other.transform.GetComponent<PlayerManager>().isBallContacted = true;
             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);
                 }
             }
             tr.enabled = true;
             Invoke("DisableTrail", 0.6f);
         }
     }
     public void PitchBall()
     {
         rb.isKinematic = false;
         float rand = Random.Range(0f, 2f);
         float randBall = Random.Range(0f, 2f);
         float randZValue;
         if (randBall < 1)
         {
             randZValue = Random.Range(-15f, -5f);
         }
         else
         {
             randZValue = Random.Range(5f, 15f);
         }
         if (rand < 1)
         {
             rb.velocity = new Vector3(10.0f, 0.0f, randZValue) * startermodifier;
         }
         else
         {
             rb.velocity = new Vector3(-10.0f, 0.0f, randZValue) * startermodifier;
         }
     }
     void GetTargetInfo()
     {
         Vector3 direction;
         Plane basePlane;
         if (rb.velocity.x > 0)
         {
             movementDirection = PlayerLine.right;
             direction = Vector3.right;
             basePlane = leftPlane;
         }
         else
         {
             movementDirection = PlayerLine.left;
             direction = Vector3.left;
             basePlane = rightPlane;
         }
         float enter = 0f;
         Ray ray = new Ray(transform.position, direction);
         if (basePlane.Raycast(ray, out enter))
         {
             sideTarget = ray.GetPoint(enter);
         }
     }
     void StayInbounds()
     {
         transform.position = new Vector3(
             Mathf.Clamp(transform.position.x, leftBound, rightBound),
             transform.position.y,
             Mathf.Clamp(transform.position.z, bottomBound, topbound));
     }
     public void StopRound()
     {
         tr.enabled = false;
         if (!GameManager.isLevelFinished)
         {
             rb.velocity = Vector3.zero;
             rb.isKinematic = true;
             Invoke("ResetBallPosition", 0.5f);
             Invoke("PitchBall", 2f);
         }
     }
     void SetScore(string name)
     {
         if (name == "Goal_Left")
         {
             if (!lm.isLeftPlayerImmune)
             {
                 GameManager.UpdateScore(0, 1);
                 lm.onScored();
             }
         }
         else
         {
             if (!lm.isRightPlayerImmune)
             {
                 GameManager.UpdateScore(1, 0);
                 lm.onScored();
             }
         }
     }
     void DisableTrail()
     {
         tr.enabled = false;
     }
     void ResetBallPosition()
     {
         transform.position = Vector3.zero;
     }
     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);
             }
         }
      }
}

Kodu incelediysen farkedeceksin ki GetTargetInfo metodunu ve rightPlane ve leftPlane meselelerini henüz anlatmadım. Burasını yapay zekada konuşuruz.

Bonus: Unity Fiziği ve RigidBody

Konuya başlamadan önce bulduğum iki makaleyi seninle paylaşmak istiyorum. Çünkü genel mantığı çok iyi anlatıyorlar. Ben de oranın özetini seninle paylaşacağım. Önce blog linkleri:

Interpolate ve Extrapolate kavramları aslında matematikten geliyor. Interpolate, belirli aralıklarda (diyelim 10 sn’de bir) hareket eden bir cismin iki aralık arasındaki bir anda (diyelim ki 12. sn) konumunu tespit etmek için iki ölçüm arasını hesaplıyor. Örnekle daha iyi anlayacaksın:

Interpolate Örneği: 10 mt/sn giden bir cismi biz her on saniyede bir ölçen bir aletle takip edelim. Başlangıçta 0 konumunda olacak, 10. sn’de 100 metrede, 20. sn’de 200 metrede olacak. Peki 12. saniyede nerede olduğunu nasıl bileceğiz? Cismin hızı her an değişebilir. Biz bunu bilmiyoruz. Elimizde de sadece bu alet var. Yapabileceğimiz tek bir şey var: 10. ve 20 saniyelerdeki konumunu alıp buradan hızı hesaplarız ve 12. saniye ile bu hızı çarparak bulunduğu konumu tespit edebiliriz ki sonuç 120 çıkar. Tekrar şunu vurgulamak istiyorum, cismin hızını bilmiyoruz, her an değişebilir. Tabii yönü de, misal geri dönebilir.

Interpolate eldeki verileri değerlendirdiği için neredeyse kesin (ters dönebilir, hızı değişebilir vs) bir sonuca varabiliriz bu açıdan güçlü bir yöntem. Fakat sıkıntı şu ki biz bunu hesapladığımızda cisim çoktan 300 – 400 metreye ulaşmış olabilir. Diyelim matematiğin kötü yavaş çarptın çıkardın bu nokta 1 km bile olabilir. O yüzden anlık tepki vermemiz imkansız. Buna rağmen Unity bunu kullanıyor ve diğer seçeneğe göre daha iyi sonuçlar veriyor. Nasıl mı? Fizik updatelerini 1 – 2 frame geç başlatarak. Yani ekranda gördüğümüz topu aslında fiziksel vektör 1 – 2 frame önce etkilemişti. Ama sen gecikmeli olarak görüyorsun bunu. Misal oyunda karekterin aslında ölüyor (çünkü normal update’de sen ölmesini tanımlamıştım) ama ekranda öldüğü yere yeni geliyor çünkü render gecikmiş fiziğin fix update’ne göre gösteriyor pozisyonunu sana. Sen de garibim, niye can basamadım diye hayıflanıyorsun helvasını yapman gerekirken. Tamam şimdi abartılı bir örnek oldu bu, çünkü bahsettiğimiz 1-2 framelik gecikme aslında saniyenin belki 30’da belki 50’de biri. Kodlarken aklında olsun diye biraz abartmak istedim.

Extrapolate ise önceki ölçümlerde elde ettiği veriye göre daha henüz gerçekleşmemiş bir noktayı tespit etmeye çalışan bir yöntem. Eğer yukarıda bahsettiğimiz cisim sabit bir hızda gitmeye devam ederse extrapolate yaparak görüntüye aktaracağımız pozisyon ile fixedupdate el ele gidecektir. Ama hızı değiştiğinde renderlanan görüntü tamamen yanlış olacak ve olduğundan ileride veya geride bir pozisyonda objeyi gösterecektir. Çünkü temelinde o render olduğu sırada aslında fiziğin güncellemesi yani fixedupdate gelmemişti. Bir sonraki framede güncelleme geldiğinde pozisyon hatalı olduğu için asıl pozisyonunda tekrar renderlanacak. Bu ise o gördüğümüz titreşme görüntüsü, glitch’e neden olacak. Bu sıkıntısıyla birlikte bu yöntemin asıl sorunu ise önüne aniden çıkan başka bir cisimle (bizim oyunumuzda topa vuran oyuncu) karşılaşmasında yatıyor. Doğru hesaplayamadığı için objenin içinden geçme, yüzeyine takılma ile muhattap olacağız. Bu da gayet can sıkıcı bir durum. Yaptığım testlerde sabit cisimlere karşı bile yüksek hızlarda yanlış hesaplayabildiğini gördüm. O yüzden dikkatli kullanmanızı tavsiye ediyorum.

Gelelim Collision Detection sistemine! Malum burada discrete, continuous, continuous dynamic ve continuous speculative seçenekleri karşımıza çıkıyor. Bu son iki seçenek sadece 3D’de mevcut. Aslında olay çok basit. 2D oyunlarda toplamda gönderilecek ışın (RayCast) iki eksendeki (X,Y) noktalardan oluşurken 3D’de bunun sayısı çok çok daha fazla. Her 3 ekseni de (X,Y,Z) kontrol etmek zorunda Unity. Bu ciddi anlamda işlemciyi zorlayabilen bir olay. O yüzden alternatif yöntemler sunarak işlemciyi daha az zorlama şansı veriyor sana. Ki bence muhteşem bir şey bu! Şimdi istersen biraz daha detaylı bakalım bunlara.

Discrete adı gibi ayrık bir inceleme yöntemi kısaca. Yaptığı şey çok basit her fixedupdate döngüsünde objenin bulunduğu yere bakıyor ve herhangi bir çarpışma var mı yok mu onu kontrol ediyor. Düşük hızlarda gayet performans dostu bir yöntem.

Continuous ise objenin her update arasında aldığı yolu kesintisiz bir şekilde taramasına neden oluyor. Buna tünel çözümü de diyorlar. Discrete’deki bağımsız iki nokta yerine burada objenin boyutunda bir tüneli tarıyor sistem. İşte yükü oluşturan mekanizma bu. Bu iki seçenek için Unity şunları öneriyor:

  • Eğer kamera objeyi takip ediyorsa continuous kullan, render dışında bir alansa discrete kullanabilirsin.
  • Eğer hızlı giden bir obje ise continuous kullan, yavaş giden bir objede discrete ihtiyacını fazlasıyla görecektir.

Sıra saf, dinamik ve spekülatif continuous’da:

  • Continuous, sadece rigidbody ile statik collider (içinde rigidbody olmayan) objelerin çarpışmasına bakıyor.
  • Continuous Dynamic ise, Discrete ve Spekülatif olmayan bütün rigidbodyleri ve statik colliderların çarpışmasını denetliyor.
  • Continuous Speculative ise her türlü colliderı olan rigidbody, static vs ne varsa hepsini kontrol ediyor.

Aslında buradan da anlayacağın üzere işlemci yükü spekülatife doğru gittikçe artıyor. Olay özetle bundan ibaret. Bunu kısaca anlatan bir tablo ile (yukarıdaki bloglardan aldığım) bu bloga bir son verelim diyorum. Gelecek hafta power upları anlatacağım yeni bir blog ile görüşmeyi sabırsızlıkla bekliyor olacağım ben. Bir sonraki yazımıza kadar muhteşem bir hafta geçirmen temennilerimle…

Advertisements

Get Just Another Game: Pong

Leave a comment

Log in with itch.io to leave a comment.