Klonluyoruz – Unity 3D’de Flappy Bird Yapımı

Yayınlandı: 17 Mayıs 2014 yasirkula tarafından Oyun Tasarımı, UNITY 3D içinde

GÜNCELLEME (28.10.2019): Ders C#’a çevrildi.

Hepinize merhaba,

Bu derste, popüler Flappy Bird oyununu Unity‘de sıfırdan tekrar programlamaya çalışacağız. Oyunumuzda bulunacak özellikler şunlar:

  • Ekrana tıklayınca kuşun zıplaması
  • Kuşun kanat çırpması (bir animasyona sahip olması)
  • Engellerin gerçek-zamanlı rastgele şekilde oluşturulması
  • Engele çarpınca ölmek
  • Yüksekskorun cihazda kayıt altına alınması
  • Çeşitli ses efektleri

Projenin bitmiş hali: https://app.box.com/s/pduuj3duocmx4c8izxjj (Alternatif link)

Derse başlamadan önce, Unity’nin arayüzüne ve script yazmaya az da olsa aşina olmanız çok işinize yarayacaktır.

Öncelikle yeni bir Unity projesi oluşturun. Projeye istediğiniz ismi verin ve “Template” olarak 2D‘yi seçin.

Eğer şimdi Game paneline geçiş yaparsanız arkaplanın boğuk bir mavi renkte olduğunu göreceksiniz. İsterseniz biz bu rengi biraz açalım. Bunun için sahnenizdeki Main Camera‘yı seçip Background değerini 28,191,214 yapın:

resim1

Başta oyunun görsellerini bir şekilde kendim hazırlamayı düşünüyordum ama sonradan bu işte yetersiz olduğumu fark ettim ve lanica.co/flappy-clone/‘dan oyunun bir klonunun görsellerini aldım. Klon oyunumuzda da bu görselleri kullanacağız. Bu projeyi öğrenme amaçlı yapacağımız için sorun çıkmayacaktır. Ben proje için gerekli tüm görselleri ve sesleri bir Winrar arşivinde depoladım. Siz direkt onu indirin: https://app.box.com/s/kct22wtzp25s4bi2h1ag (Alternatif link)

NOT: Bir alttaki paragrafı okumadan bu notu okumanızın önemli olduğunu düşünüyorum. Unity’nin yeni sürümleriyle beraber Project panelinin tarzı değişti. Artık iki farklı şekilde görebiliyoruz Project panelini: tek hücreli ve iki hücreli olarak. Yeni bir proje açtığınız zaman Project paneli otomatik olarak iki hücreli şekilde gelir ama ben bu yeni stili sevmiyorum ve eski tek hücreli stili kullanıyorum. Dersi anlatırken de tek hücreli stili kullanarak anlatıyorum. Eğer herhangi bir sıkıntı yaşamak istemiyorsanız siz de tek hücreli Project paneline geçiş yapın. Bunu yapmak çok kolay: Project panelinin sağında bir kilit simgesi, onun da sağında bir menü simgesi var. İşte o menü simgesine tıklayın ve One Column Layout seçeneğini seçin:

resim2

Şimdi Project panelinde Create-Folder yolunu izleyerek Sprites adında yeni bir klasör oluşturun ve Winrar arşivindeki sprite’leri bu klasöre atın. Eğer sürükle-bırak işe yaramıyorsa Assets-Import New Asset… yolunu izleyerek tüm sprite’leri tek tek import edin.

Sonra flappy1 spritesini seçip Inspector panelinde yer alan Compression kısmını None olarak değiştirin ve alttan Apply butonuna basın. Bu işlemi flappy2 ve flappy3 spriteleri için de yapın. Eğer bunu yapmazsak kuş biraz eğim aldığında etrafında istenmeyen siyah çizgiler oluşuyor ama artık böyle bir sorun yaşamayacağız.

Kuşu Oluşturmak ve Kanat Çırpmasını Sağlamak

Projenin ilk aşamasında planım kuş objemizi oluşturmak ve ona animasyon vermek. Kuşumuzun kanat çırpma animasyonu üç kareden oluşuyor. İlk karede kanat yukarı kalkmış, ikinci karede kanat ortada ve son karede kanat aşağı inmiş vaziyette. Eğer animasyonumuzu 1-2-3-1-2-3-… diye oynatırsak kuşun kanadı yukarıdan aşağıya iner ve ardından direkt yukarıya geri zıplar. ama eğer 1-2-3-2-1-2-3-… diye zikzak yaptırarak oynatırsak kanat önce aşağı iner, sonra yukarı çıkar ve sonra yine aşağı iner ve bu böyle devam eder. Fark edeceğiniz üzere ikinci yol daha sağlıklı. Bu yüzden bu yolu izleyeceğiz.

Kuşa animasyonunu vermek için script yazacağız. Scriptin başlıca avantajı, kuşun animasyon hızını istediğimiz gibi değiştirebilmek ve animasyonun oynama sırasına karar verebilmek (1-2-3-2-1-… diye). O halde önce kuşumuzu oluşturalım. GameObject-2D Object-Sprite yolunu izleyin. Ardından “flappy1” sprite’sini Assets klasöründen tutup Sprite Renderer‘daki Sprite kısmına sürükleyin:

resim3

Bu aşamada kuş ekranınızda belirecektir. Eğer belirmemişse Scene panelinde biraz dışarı doğru zoom yapın. Ya da Hierarchy panelindeki New Sprite objesini seçip fareyi Scene paneline getirin ve klavyeden F tuşuna basın. Kamera kuşa odaklanacaktır. Şimdi Inspector panelinden New Sprite‘ın adını FlappyBird olarak değiştirin. Hemen sonrasında ise Transform componentinin sağında yer alan dişli çarka tıklayıp Reset‘e dokunarak kuşu 3 boyutlu uzayın tam merkezine (origin) yerleştirin:

resim4

Bu işlemin ardından Scene panelinde F tuşuna basarak kuşa tekrar odaklanmanız gerekebilir.

Şimdi sıra geldi kuşa animasyon vermeye. Bunun için yeni bir C# script oluşturun (Assets-Create-C# Script) ve adını KusAnimasyon koyun. Bu scripti Project panelinden tutup sürükleyerek Hierarchy‘deki FlappyBird objesine atayın. Artık scripti düzenlemeye hazırız. Scripte çift tıklayarak onu açın ve içini şöyle değiştirin:

using UnityEngine;

public class KusAnimasyon : MonoBehaviour
{
	public int saniyedeKareSayisi = 10;
	public Sprite[] animasyonKareleri;

	private SpriteRenderer spriteRenderer;

	private float sonrakiAnimasyonDegismeAni;
	private bool animasyonYonu = true;
	private int mevcutAnimasyonKaresi = 0;

	void Start()
	{
		if( animasyonKareleri.Length < 2 )
			Destroy( this );

		spriteRenderer = GetComponent<SpriteRenderer>();
		sonrakiAnimasyonDegismeAni = Time.time + 1f / saniyedeKareSayisi;
	}

	void Update()
	{
		if( Time.time >= sonrakiAnimasyonDegismeAni )
		{
			if( animasyonYonu )
			{
				if( mevcutAnimasyonKaresi == animasyonKareleri.Length - 1 )
				{
					mevcutAnimasyonKaresi--;
					animasyonYonu = false;
				}
				else
				{
					mevcutAnimasyonKaresi++;
				}
			}
			else
			{
				if( mevcutAnimasyonKaresi == 0 )
				{
					mevcutAnimasyonKaresi++;
					animasyonYonu = true;
				}
				else
				{
					mevcutAnimasyonKaresi--;
				}
			}

			spriteRenderer.sprite = animasyonKareleri[mevcutAnimasyonKaresi];
			sonrakiAnimasyonDegismeAni = Time.time + 1f / saniyedeKareSayisi;
		}
	}
}

Bu scripti hızlıca açıklamaya çalışayım. En başta 6 adet değişken tanımlıyoruz:

saniyedeKareSayisi: Bu değişkenin varsayılan değeri 10. Bunun anlamı, bir saniyede kuş animasyonundaki kareler arasında toplam 10 kere geçiş olacak. Bu değeri artırırsanız kuş daha hızlı kanat çırpar. Kendisi public bir değişken olduğu için Inspector’dan değerini değiştirerek en uygun değeri kolayca bulabilirsiniz.

animasyonKareleri: Bu bir Sprite dizisi (array). Flappy Bird’in her bir animasyon karesine bir sprite diyoruz. Yani bu değişken kuşun kanat çırpma animasyonundaki kareleri depoluyor. Elimizde üç tane sprite var: flappy1, flappy2 ve flappy3. Şimdi Inspector’da Kus Animasyon (Script) yazan yerin altındaki Animasyon Kareleri‘nin üzerine tıklayın. Kendisi genişleyecektir. Gelen değerlerden Size‘ı 3 yapın. Bu değer array’de toplam kaç eleman depolayacağınızı belirtir. Şimdi sırayla flappy1, flappy2 ve flappy3 sprite’lerini Project panelinden sürükleyerek bu Element’lerin üzerine bırakın:

resim5

Eğer şu anda Play butonuna basarsanız kuşun ekranda kanat çırptığını göreceksiniz. Biz değişkenleri tanımaya devam edelim.

spriteRenderer: FlappyBird objemizdeki Sprite Renderer component’ini tutar. Bu değişken vasıtasıyla component’e atalı Sprite’ı değiştirerek ekranda gördüğümüz sprite’ın değişmesini sağlıyoruz.

sonrakiAnimasyonDegismeAni: Bir sonraki animasyon karesini hangi saniyede oynatacağımızı (salisesiyle beraber) depolar. Oyunda o saniyeye ulaştığımızda kuşun animasyonundaki bir sonraki kareye geçiş yapılır ve bu sonrakiAnimasyonDegismeAni değişkeninin değeri artar.

animasyonYonu: Değeri true ise kuşun animasyonu Element 0’dan Element 2’ye doğru akarken değeri false olursa kuşun animasyonu Element 2’den Element 0’a doğru akar. Bu değişkeni kullanma sebebimiz, kuşun animasyonunu 1-2-3-2-1-2-3-… şeklinde oynatacak olmamız.

mevcutAnimasyonKaresi: Mevcut kuş sprite’ının animasyonKareleri dizisindeki kaçıncı sprite olduğunu depolar. Değeri 0 ise o anda animasyonun Element 0’ıncı karesinde, 1 ise Element 1’inci karesinde, 2 ise Element 2’nci karesinde yer aldığımızı anlarız.

Gelelim Start fonksiyonuna. Burada yer alan animasyonKareleri.Length değeri, animasyonKareleri array’inin büyüklüğünü, yani Inspector’dan girdiğimiz Size değerini bize döndürür. Eğer bu değer 2’den küçükse Destroy(this); komutu ile scripti FlappyBird objesinden siliyorum çünkü Size değerinin 2’den küçük olması demek, animasyon olmaması demektir ve bu durumda bu scriptin gereksiz çalışmasına gerek yok.

NOT: Destroy(this) komutu ile Destroy(gameObject) komutunu karıştırmayın. Destroy(this) komutu, bir scripti objeden atmaya yararken Destroy(gameObject) komutu scriptin yazıldığı objeyi yok etmeye yarar.

Start fonksiyonunda ikinci olarak, GetComponent fonksiyonu ile spriteRenderer değişkenine değer olarak objedeki Sprite Renderer component’ini veriyoruz. Son olarak da sonrakiAnimasyonDegismeAni değişkenine ilk değerini veriyoruz. Time.time komutu, oyun ilk başladığında 0 değerini alır ve oyun başlatıldıktan itibaren kaç saniye geçtiğini depolar. Örneğin oyunu açtıktan tam 5 saniye sonra Time.time’ın değeri 5 olur. Biz de tam üzerinde bulunduğumuz Time.time saniyesine 1f / saniyedeKareSayisi‘ni ekliyoruz. Burada 1’in yanındaki f harfi, sayının float olduğunu belirtir. İstersek oraya 1.0f de yazabiliriz; uzun lafın kısası float değerlerle uğraşırken sayının sağına f harfi koymak zorundayız. Yaptığımız işlem basit: eğer 1 saniyede saniyedeKareSayisi kadar kare gösterilecekse, bir kare 1 / saniyedeKareSayisi kadar saniyede gösterilir. Eğer saniyedeKareSayisi‘nı 1f’e değil de 1’e bölseydik bu bir integer bölmesi olurdu ve bulunan sayının küsüratlı kısmı silinirdi. Yani bölme işlemi hep 0 döndürürdü.

Update fonksiyonu oldukça basit. Önce üzerinde bulunduğumuz saniyenin (Time.time) sonrakiAnimasyonDegismeAni’ndan büyük olup olmadığına bakıyoruz. Eğer büyükse anlarız ki animasyonun sonraki karesine geçmenin zamanı gelmiştir. Eğer animasyonYonu true ise animasyonu Element 0’dan Element 2’ye doğru oynatıyoruz. Ama eğer o an Element 2’deysek (mevcutAnimasyonKaresi == animasyonKareleri.Length – 1) o zaman animasyonYonu’nü tersine çeviriyoruz ve mevcutAnimasyonKaresi’ni artırmak yerine 1 azaltıyoruz. Animasyonun sonraki oynatılacak karesine karar verdikten sonra ise, objemizin SpriteRenderer component’indeki Sprite‘ın değerini yeni animasyon karesiyle değiştiriyoruz ve sonrakiAnimasyonDegismeAni‘nın değerini güncelliyoruz. Umarım her şey anlaşılmıştır. Unutmayın: bu ders hiç script bilgisi olmayanlar için değildir, eğer buraya kadar dediklerimden hiç bir şey anlamadıysanız önce başka tutorialler vasıtasıyla biraz C# aşinalığı kazanmalısınız.

Kuşu oluşturup ona animasyon verdiğimize göre sonraki kısma geçebiliriz. Önce şimdiye kadar yaptığımız aşamayı kaydedelim. Bunun için File-Save Scene yolunu izleyin ve sahneye Oyun adını verin.

Kuşu Zıplatmak, Yerçekimi ve Kuşun Dönmesi

KusHareket adında yeni bir C# script oluşturun, scripti FlappyBird objesine atın ve içini şöyle değiştirin:

using UnityEngine;

public class KusHareket : MonoBehaviour
{
	public float yercekimi = 4f;
	public float ziplamaGucu = 2.5f;
	public float yatayHiz = 1.5f;
	private float dikeyHiz = 0f;

	void Update()
	{
		dikeyHiz -= yercekimi * Time.deltaTime;

		if( Input.GetMouseButtonDown( 0 ) )
			dikeyHiz = ziplamaGucu;

		float egim = 90 * dikeyHiz / yatayHiz;

		if( egim < -50 ) egim = -50;
		else if( egim > 50 ) egim = 50;

		transform.eulerAngles = new Vector3( transform.eulerAngles.x, transform.eulerAngles.y, egim );
		transform.Translate( new Vector3( 0, dikeyHiz, 0 ) * Time.deltaTime, Space.World );
	}
}

Eğer Play tuşuna basarsanız kuşun bu kod vasıtasıyla yerçekimine kapılıp aşağı düştüğünü ve ekrana tıklayınca biraz yükseldiğini göreceksiniz. Kuşun dikey hızına bağlı olarak eğiminin de değiştiğini fark etmiş olmalısınız. Şimdi kodu inceleyelim:

yercekimi: Kuşa etki eden yerçekiminin gücü. Değerini artırırsanız kuş daha çabuk yere düşer.

ziplamaGucu: Ekrana dokununca kuşun dikey hızını bu değere eşitliyoruz. Eğer değeri artırırsanız ekrana tıklayınca kuş daha yukarı zıplar.

yatayHiz: Kuşun yatay eksende (x-ekseni) ne kadar hızlı gideceğini belirleyen değer. Henüz tam bir işe yaramıyor.

dikeyHiz: Kuşun dikey eksende (y-ekseni) ne kadar hızlı hareket ettiğini belirleyen değer. Bu değişkenin private olduğuna dikkat edin. Bunun sebebi, yerçekiminin ve ekrana tıklamalarımızın etkisiyle değerinin script vasıtasıyla değiştiriliyor olması. Yani değişkenin Inspector’da gözükmesine gerek yok.

Update fonksiyonuna girecek olursak; ilk satırda dikeyHiz‘a yerçekiminin etki ettiğini görebilirsiniz. Burada Time.deltaTime devreye giriyor. Eğer bilmiyorsanız diye kısaca açıklayayım. Update fonksiyonunda bir değişkenin değerini saniyede 1 artırmak istiyorsanız o değişkene Time.deltaTime eklerseniz. Eğer değerini 5 azaltmak istiyorsanız o değişkenden 5 * Time.deltaTime çıkarırsınız. Yani Time.deltaTime, Update fonksiyonunda bir değişkenin değerinin bir saniyede belli bir miktar değiştirilmesine yardımcı olur. Biz Update fonksiyonunda dikeyHiz’dan yercekimi * Time.deltaTime’ı çıkarıyoruz. Bunun anlamı, dikeyHiz’ın değeri her saniyede yercekimi kadar azalacaktır.

İkinci olarak if( Input.GetMouseButtonDown( 0 ) ) ile ekrana o anda farenin sol tuşuyla tıklayıp tıklamadığımıza bakıyoruz ve eğer tıklamışsak kuşun dikeyHiz‘ını ziplamaGucu‘ne eşitliyoruz. Bu da kuşun yönünün aniden yukarı doğru değişmesine sebep oluyor.

Devam edecek olursak, kodda egim diye bir değişkenin tanımlandığını görebiliriz. Bu değişken, FlappyBird objemizin, dikeyHiz‘ın değerine göre eğim almasına yardımcı oluyor. Önce dikeyHiz’ın yatayHiz’a oranını buluyor, sonra bu değeri 90 ile çarparak eğimi bir açıya çeviriyoruz. Ardından iki if koşulu ile bu eğimi -50 ile 50 derece arasında tutuyoruz. Eğer açı 50 ise buradan anlayabiliriz ki kuşun burnu 50 derece yukarı bakıyordur. Eğer bu iki if koşulunu silersek kuşa yerçekimi etki ettikçe kuş dönmeye devam edecektir ve hoş olmayan bir görüntü ortaya çıkacaktır.

Elimizde bir egim değişkeni var. Şimdi bu değişkeni kuşun Transform component’ine uygulayarak kuşun sprite‘sine eğim vermeliyiz. Bunun için transform.eulerAngles değişkenini kullanıyoruz. Bu değişken, Inspector’da Transform component’i altında yer alan Rotation değerlerini temsil etmektedir. 2 boyutlu uzayda bir objeye eğim vermek için daima bu Rotation’ın Z değeri değiştirilmelidir. Biz de bunun için transform.eulerAngles’ın x ve y değerlerini aynen bırakıyoruz.

Son olarak, transform.Translate komutu ile objeyi dikey eksende (y-ekseni) saniyede dikeyHiz kadar hareket ettiriyoruz (burada da Time.deltaTime kullandığıma dikkat edin). Bu kodda dikkatinizi Space.World parametresi çekmiş olabilir. Hemen açıklayayım: normalde Translate fonksiyonuna girdiğiniz değerler, objenin eğimine göre değiştirilerek öyle objeye uygulanır. Yani mesela Translate vasıtasıyla objeyi yukarı yönde 5 birim hareket ettiriyorum diyelim. Eğer objenin herhangi bir eğimi yoksa kod objeyi ekranda gerçekten yukarı yönde 5 birim hareket ettirir. Ama eğer obje bir eğime sahipse, yani mesela 90 derece sağa dönmüşse (kuşun burnu yere bakıyorsa) o zaman objeyi yukarı yönde 5 birim hareket ettirmeye çalışınca obje kendisine göre yukarı ama bize göre sağa doğru (burnu aşağı bakan bir kuşun yukarı yönü bize göre sağ olur) hareket eder. İşte Translate fonksiyonunun sonuna Space.World ibaresini ekleyerek bu durumu engelliyor ve kuşun eğimi ne olursa olsun hep bize göre yukarı yönde hareket etmesini sağlıyoruz.

Kuşu neden henüz yatay eksende hareket ettirmediğimi merak ediyor olabilirsiniz. Öyle yaptım çünkü Flappy Bird oyununda bildiğiniz üzere kamera kuşu yatay eksende takip eder ama dikey eksende takip etmez. Yani kuş ne kadar yukarı ya da aşağı hareket ederse etsin kamera yukarı-aşağı oynamaz. Unity’de kameranın kuşu takip etmesinin en kolay yolu kuşu kameranın parent objesi olarak atamaktır. Böylece kamera hep kuşu takip edecektir. Ama bunun dezavantajı ise kamera kuşu gerçek anlamda takip edecektir, yani kuş yukarı-aşağı hareket ettikçe onunla beraber yukarı-aşağı inecek, kuşun eğimi değiştikçe kameranın da eğimi değişecektir. İşte bu duruma engel olmak için (kameranın kuşu sadece yatay eksende takip etmesi ve kuşla beraber eğim almaması için) biraz farklı bir hiyerarşi oluşturacağız.

GameObject-Create Empty ile boş bir GameObject oluşturun. Empty GameObject’in Inspector‘unda Transform component’inin sağındaki dişli çarka tıklayıp Reset deyin. Sonrasında Hierarchy‘den FlappyBird objesini bu objenin üzerine sürükleyerek Empty GameObject’i FlappyBird’ün parent’ı yapın. Aynı işlemi Main Camera‘ya uygulayarak Empty GameObject’i Main Camera’nın da parent’ı yapın:

resim6

Artık FlappyBird objesi ve kameramız Empty GameObject‘i takip edecekler. Eğer kuşumuzun yatayHiz‘ını bu Empty GameObject’e uygularsak amacımıza ulaşmış olacağız: Empty GameObject sadece yatay eksende hareket etmiş olacak ve beraberinde FlappyBird ile Main Camera’yı da yatay eksende taşıyacak (çünkü onların parent’ı). Buna ek olarak FlappyBird kendi başına dikey eksende de hareket edecek (dikeyHiz sayesinde) ama kamera bu dikey eksendeki hareketi takip etmeyecek çünkü kameranın FlappyBird ile hiçbir direkt bağlantısı yok. O halde yapmamız gereken şey, kuşun yatayHiz’ını Empty GameObject’e vermek. Bunun için KusHareket scriptinde en alttaki Translate fonksiyonunun bir altına şu satırı ekleyin:

transform.parent.Translate( yatayHiz * Time.deltaTime, 0, 0 );

Yazdığımız bu kod ile FlappyBird’ün parent’ına (yani Empty GameObject‘e) ulaşıp onu yatay eksende saniyede yatayHiz kadar hareket ettiriyoruz. Burada Space.World kullanmamıza gerek yok çünkü Empty GameObject hiçbir zaman eğim almıyor ve bu yüzden ona göre sağ hep bize göre de sağ oluyor.

Şu anda arkaplanda başka obje olmadığı için kuşun yatayda hareket edip etmediğini anlayamayabilirsiniz. Bunu test etmenin en basit yolu sahnede geçici bir küp oluşturmak: GameObject-3D Object-Cube. Şimdi Play butonuna basarsanız küp objesinin sola doğru kaydığını görebilirsiniz. Harika!

Bir sonraki aşamaya geçmeden önce küpü silmeyi ve ardından File-Save Scene ile sahneyi kaydetmeyi unutmayın.

Arkaplanı Oluşturmak ve Sonsuza Dek Devam Etmesini Sağlamak

Aslında bu tutoriale başlarken aklımda arkaplana birşey koymak yoktu. Hatta o yüzden dersin başında kameranın arkaplanındaki rengi açık maviye çevirdik. Ama bu kısımda kararımı değiştirdim ve bu yüzden de birlikte oyuna bir arkaplan ekleyecek ve arkaplanın hiç bitmemesini, sürekli kendini tekrar etmesini sağlayacağız. Bu işi ise tamamen script yardımıyla yapacağız.

Arkaplan iki ayrı sprite’den oluşmakta: gökyüzü ve toprak. Bu ikisinin ayrı sprite’ler olmasının sebebi, gökyüzünün toprağa nazaran daha yavaş hareket edecek olması. Buna parallax scrolling deniyor oyun terminolojisinde. Yani yakındaki arkaplan objelerinin uzaktaki arkaplan objelerine nazaran daha hızlı hareket etmesine deniyor. Bu da daha gerçekçi ve hoş bir görüntü oluşturuyor.

Bizim yazacağımız script, oyun başladığında ekrana gökyüzü ve toprak sprite’lerinden ne kadar sığıyorsa o kadarını güzelce yerleştirecek ve kuşumuz sağa doğru hareket ettikçe kameranın dışında kalan soldaki arkaplan sprite’lerini en sağa taşıyarak arkaplanın sanki hiç bitmiyormuşçasına kendini tekrar etmesini sağlayacak.

İlk önce gökyüzü ve toprak spritelerini uygun şekilde düzenleyelim. Bunun için Project panelindeki Sprites klasörünüzden gokyuzu sprite’ını seçin ve Inspector’dan Pivot özelliğini Top Left olarak değiştirin. Ardından aşağıdan Apply butonuna basın. Aynı şeyi toprak sprite’ı için de yapın:

resim7

Burada yaptığımız şey çok basit: artık gokyuzu ve toprak objelerini Unity’e attığımızda, bu objelerin Inspector’daki Transform component’inde gördüğümüz position değerleri, tam sol üst köşelerinin pozisyonunu temsil edecek. Eskiden Pivot‘un değeri Center iken position değeri objenin merkezini temsil ediyordu. Böyle yapmamız kod yazarken işimize yarayacak.

Şimdi gokyuzu ve toprak spritelerini birer prefab‘a çevirelim. Bunun için önce gokyuzu spritesini Project panelinden sürükleyerek Scene paneline bırakın. Sahnede gökyüzünü gösteren yeni bir GameObject oluşacaktır. Bu objeyi seçin ve Transform component’indeki değerleri sıfırlayın (dişli çarka tıklayıp Reset‘e basın). Ardından Sprite Renderer component’indeki Order in Layer‘ın değerini -10 yapın. Ekrandaki sprite objelerinin çizilme sırasını Order in Layer belirler. Bu değer ne kadar büyükse o obje o kadar sonra çizilir. FlappyBird’ün bu değeri 0, yani önce arkaplan, sonra kuş çizilecek ve böylece kuşun arkaplanın arkasında kalmasının önüne geçeceğiz.

Şimdi gokyuzu objesini Hierarchy panelinden tutup sürükleyerek Project paneline bırakın. Harika! Artık gokyuzu prefabımız oluştu. Aynı süreci toprak sprite’ı için de tekrarlayın ancak onun Order in Layer‘ını -5 yapın. Böylece objelerin ekranda çizilme sırası Gökyüzü->Toprak->FlappyBird şeklinde olacak.

İşiniz bitince Project panelinde Prefablar isminde yeni bir klasör oluşturup toprak ve gokyuzu prefab’larını içine atın. Böylece projemiz daha düzenli olacak. Artık sahnedeki gokyuzu ve toprak objelerine ihtiyacımız kalmadı, onları Hierarchy panelinden veya Scene panelinden seçerek silin.

resim8

NOT1: Üstteki resimde Sprites klasöründe ustEngel sprite’ı eksik. Ama sizde ustEngel sprite’ının da olması lazım. Hata benim resmimde, sizin yanlış yaptığınız bir şey yok.

NOT2: Bu safhada Main Camera‘nın Transform component’indeki position değerinin 0,0,-10 olduğundan emin olun yoksa script düzgün çalışmayabilir.

Artık arkaplan script’ini yazmaya hazırız. Arkaplan adında yeni bir C# script oluşturup scripti Main Camera objesine atayın. Ardından içeriğini şöyle değiştirin:

using UnityEngine;

public class Arkaplan : MonoBehaviour
{
	public GameObject gokyuzu;
	public GameObject toprak;

	private int arkaplanSayisi;

	private Vector2 kameraUnityEbatlar;
	private Vector2 gokyuzuUnityEbatlar;
	private Vector2 toprakUnityEbatlar;

	void Start()
	{
		Camera kamera = GetComponent<Camera>();

		gokyuzuUnityEbatlar = gokyuzu.GetComponent<SpriteRenderer>().sprite.rect.size / 100;
		toprakUnityEbatlar = toprak.GetComponent<SpriteRenderer>().sprite.rect.size / 100;

		kamera.orthographicSize = ( gokyuzuUnityEbatlar.y + toprakUnityEbatlar.y ) / 2;
		arkaplanSayisi = Mathf.CeilToInt( ( kamera.orthographicSize * 2 * kamera.aspect ) / gokyuzuUnityEbatlar.x ) + 1;

		kameraUnityEbatlar = new Vector2( kamera.orthographicSize * kamera.aspect, kamera.orthographicSize );

		for( int i = 0; i < arkaplanSayisi; i++ )
		{
			float xKoordinati = transform.position.x - kameraUnityEbatlar.x + i * gokyuzuUnityEbatlar.x;
			Instantiate( gokyuzu, new Vector3( xKoordinati, kameraUnityEbatlar.y, 0 ), Quaternion.identity );
			Instantiate( toprak, new Vector3( xKoordinati, kameraUnityEbatlar.y - gokyuzuUnityEbatlar.y, 0 ), Quaternion.identity );
		}
	}
}

Bu script biraz karmaşık, kabul ediyorum. Ancak hayıflanmadan önce scripti kaydedin ve Main Camera‘dan Arkaplan (Script) component’indeki Gokyuzu ve Toprak kısımlarına değer olarak gokyuzu ve toprak prefablarını verip oyunu test edin.

resim9

Arkaplanın tam oturduğunu ve kameranın arkaplanı tam oturtmak için kendi boyutunu değiştirdiğini göreceksiniz. Tek sorun bir süre sonra arkaplan sona eriyor çünkü henüz geride kalan arkaplan objelerini ileriye ışınlamıyoruz. Onu sonra yapacağız. Şimdi elimden geldiğince scripti açıklamaya çalışacağım.

resim10

gokyuzu: Gökyüzü prefabının depolandığı değişken. Oyun başlarken Instantiate ile sahneye gökyüzü objeleri eklerken kullanıyoruz.

toprak: Toprak prefabının depolandığı değişken. Tıpkı gokyuzu gibi; oyun başlarken toprak objeleri oluştururken kullanıyoruz.

arkaplanSayisi: Kameraya kaç tane arkaplan objesinin sığabildiğini depoluyor. Oyun başladığında bu değişkenin değeri kadar yanyana arkaplan objesi oluşturuyoruz. Aslında değişkenin gerçek değeri kameraya sığan arkaplan objesi sayısının 1 fazlası. Çünkü oyun sırasında her zaman arkaplanın ekranda görünür olmasını, kameranın kendi açık mavi arkaplanın hiç gözükmemesini istiyoruz. Bunun için kameranın dışında kalan arkaplan objelerini soldan sağa ışınlıyoruz ama eğer bu değişkenin değeri normalden 1 fazla olmasaydı en soldaki arkaplan objesi kameranın solundan dışarı çıkıp sağına geri ışınlanana kadar kamera sağdaki diğer arkaplan objelerini geride bırakıp bir süreliğine kendi açık mavi arkaplanını gösterecekti bize. Ama bu değeri 1 artırdığımız için artık bu boşlukta kameranın mavi arkaplanı gözükmeyecek, yine bir başka arkaplan objesi gözükecek.

kameraUnityEbatlar: Son üç değişkeni açıklarken iki farklı terimden faydalanacağım ve bu terimleri yazının devamında da kullanacağım. O yüzden dikkatle dinleyin:

— 1 Uzay Birimi: Bildiğiniz gibi oyunumuzu boş bir uzayda tasarlayıp bu uzaya objeler ekleyerek sahnemizi oluşturuyoruz. Peki uzay biriminden kastım nedir? Şöyle açıklayayım: Project panelinden sahnenize bir gokyuzu sprite’si sürükleyip objenin Transform‘unun position‘ının X değerini 0 yapın. Kamerayı biraz uzaklaştırın ve şimdi gokyuzu’nün position’ının X değerini 1 yapın. Gökyüzü uzayda bir miktar sağa kaydı değil mi? İşte gökyüzünün uzaydaki bu kayma miktarına 1 uzay birimi diyoruz. (Sahnedeki gokyuzu’nü geri silmeyi unutmayın)

— 1 Sprite Birimi: Bu birim, bir sprite’ın genişliğinin kaç piksel olduğuna denk gelmiyor, hayır. Bu birim, sprite’taki kaç pikselin 1 uzay birimine denk geldiğini belirtiyor. Ve bu birimin değerini bulmak çok kolay: Hemen Project panelinden gokyuzu sprite’ını veya herhangi başka sprite’yi seçin. Inspector panelinde Pixels to Units diye bir değer var. İşte orada yazan değer tam olarak 1 sprite birimine eşit. Varsayılan olarak 1 sprite birimi 100 pikseldir. Yani bir sprite’taki 100 piksel 1 uzay birimine denk gelir. Yani tam 100 piksel genişliğinde bir sprite’tan sahneye iki tane koyarsanız ve birinin X değerini 0, ötekinin X değerini 1 yaparsanız bu iki sprite mükemmel bir şekilde yan yana otururlar. Umarım bu iki birim de anlaşılmıştır.

NOT: Sprite Birimi‘ni bu ders için ben uydurdum. Gerçekte böyle bir birim olduğunu sanmıyorum. Dersin daha rahat anlaşılması için böyle bir yola başvurdum.

Şimdi kameraUnityEbatlar değişkenini açıklamaya başlayabilirim. Bu değişken bir Vector2, yani X ve Y değerlerine sahip. Değişkenin X değerinde kameranın tam ortası ile tam solu arasının kaç Uzay Birimi‘ne denk geldiği kaydedilirken Y değerinde kameranın tam ortası ile tam tepesi arasının kaç Uzay Birimi‘ne denk geldiği kaydediliyor. Yani X değeri kameranın genişliğinin kapladığı Uzay Birimi’nin yarısını, Y değeri de kameranın yüksekliğinin kapladığı Uzay Birimi’nin yarısını depoluyor.

gokyuzuUnityEbatlar: Bu da bir Vector2. Değişkenin X değeri gokyuzu sprite’ımızın genişliğinin kaç Uzay Birimi‘ne denk geldiğini, Y değeri ise gokyuzu sprite’ımızın yüksekliğinin kaç Uzay Birimi‘ne denk geldiğini depoluyor. Bu değişkenin değerini hemen şimdi beraber hesaplayalım ki konu aklınıza daha da yatsın. Soruyorum size: 1 Sprite Birimi kaçtır? Ve soruyu sorduğum gibi umarsızca kendim cevaplıyorum: 100! Evet, 1 Sprite Birimi 100 piksele eşit (daha 3 paragraf önce gördük, unuttum demeyin). Bizim gokyuzu sprite’ımızın ebatları tamı tamına 288×414 piksel. Deminden beridir 1 Sprite Biriminin 100 piksele eşit olduğundan da bahsediyoruz. Ve 1 Sprite Biriminin tanımını hatırlayın: Spritedeki kaç pikselin 1 Uzay Birimi‘ne denk geldiğini belirten birim. E hesaplayın o zaman 288×414 pixellik gokyuzu spritemiz yatayda ve dikeyde kaç Uzay Birimi‘ne denk geliyor? Çok kolay, değil mi? Tek yapmamız gereken 288’i 100’e bölmek ve şıp diye cevabı buluyoruz: 2.88 . Evet, sprite yatayda 2.88 uzay birimine denk geliyor. Dikeyde de 414 / 100 = 4.14 uzay birimine denk geliyor. Peki bu bulduğumuz değerler ne demek? Şöyle ki eğer bir gokyuzu sprite’ını X=0 konumuna koyduysak, diğerini tam olarak X=2.88’e koyduğumuz vakit bu iki sprite tam olarak yan yana oturmuş olacak ve sprite’ımızın sağıyla solu sanki birbirini devam ettiriyormuş gibi güzel resmedildiği için yan yana duran bu iki sprite birbirini devam ettirecek.

toprakUnityEbatlar: Ve yine Vector2 türünde bir değişken! Bu değişken de artık tahmin edebileceğiniz üzere toprak sprite’ının yatayda ve dikeyde kaç Uzay Birimi‘ne denk geldiğini depoluyor. Spritenin 288×32 piksel olduğunu belirttikten sonra bu değişkenin X değerinin 2.88, Y değerinin 0.32 olduğunu ben bu cümleyi bitirmeden hesapladığınızı umuyorum.

Oh be! Değişkenleri anlatabildim sonunda. Umarım hepsini bir güzel anlamışsınızdır. Şimdi scriptteki tek fonksiyon olan Start fonksiyonuna bakalım.

İlk olarak kamera objesinin Camera component’ini GetComponent ile kamera değişkenine veriyoruz çünkü kodun devamında Camera component’ine bu değişken vasıtasıyla erişiyoruz.

Sonraki iki satırda, gokyuzuUnityEbatlar‘ın ve toprakUnityEbatlar‘ın değerlerini aldığını görüyoruz. İstesem gokyuzuUnityEbatlar’a direkt Vector2( 2.88, 4.14 ) değerini verebilirdim ve emin olun oyun yine düzgün şekilde çalışırdı. Ama ben daha genel bir çözüm yoluna gittim ve sprite’ın ebatlarını bilmediğimizi varsaydım. Bu yüzden de sprite’ın ebatlarını Unity’den kod vasıtasıyla çektim ve bu ebatları 100’e (1 Sprite Birimi) bölerek sprite’ın kaç Uzay Birimine denk geldiğini buldum. Örneğin gokyuzu sprite’ının kaç piksel genişliğinde olduğunu nasıl bulduğumu anlatayım: Bizim sprite’larımızın Sprite Renderer diye bir component’i var. Bu component o objedeki sprite’ı ve başka birkaç değeri depoluyor. Her sprite’ın ise ebatlarının depolandığı kendi değişkeni bulunur ve bu değişkenin adı rect‘tir. Yani gokyuzu’nün kaç piksel genişliğinde olduğunu bulmak için yapmam gereken, gokyuzu prefab’ındaki Sprite Renderer component’inde yer alan sprite’ye erişmek ve onun rect değişkeninin width (genişlik) değerini okumak. Bir objenin Sprite Renderer component’ine erişmek için yazmanız gereken kod da şöyle: gokyuzu.GetComponent<SpriteRenderer>(). rect değişkeninden genişliği width ile ve yüksekliği de height ile okuyabilirdik ama rect değişkeninin size‘ı bize zaten içerisinde bu değerleri tutan bir Vector2 döndürüyor (Vector2’nin x‘i width‘i, y‘si de height‘ı tutuyor). Artık bu kısmı anladığınızı varsayıyorum.

Gelelim Start‘ın bir sonraki satırına. Burada kamera.orthographicSize değişkeninin değerini ayarlıyoruz. Bu değişken de neyin nesi? Bu değişken her Orthographic render yapan kamerada bulunur ve kameranın orta noktası ile tepe noktası arasının kaç Uzay Birimi‘ne denk geldiğini depolar. Yani bizim kameraUnityEbatlar değişkeninin Y değerinde depolayacağımız değerle birebir aynı değeri depolar. Peki değişkene değerini nasıl veriyoruz? gokyuzu sprite’ının ve toprak sprite’ının kaç Uzay Birimi yüksekliğinde olduğunu topluyoruz ve çıkan değeri 2’ye bölüyoruz çünkü dediğim gibi, orthographicSize değişkeni kameranın sadece ortası ile tepesi arasındaki yüksekliğin kaç Uzay Birimi olduğunu depoluyor.

Peki arkaplanSayisi‘nı nasıl hesaplıyoruz? Burada şu bilgiyi vermem lazım: Orthographic kameralarda (Unity 2D’de varsayılan kamera tipimiz) kameranın yüksekliğinin aldığı Uzay Birimi değerini kamera.aspect isimli bir değişkenle çarparsanız, kameranın genişliğinin kaç Uzay Birimine karşılık geldiğini bulursunuz. Kameranın yüksekliğinin yarısının kaç uzay birimine denk geldiğini biliyoruz: kamera.orthographicSize. O halde bu değişkeni 2 ile çarpınca kameranın yüksekliğini, çıkan değeri de kamera.aspect ile çarpınca kameranın genişliğini bulmuş oluyoruz.

Yaptığımız şey, önce kameranın genişliğini gokyuzu sprite’ının genişliğine bölmek ve çıkan değeri Mathf.CeilToInt() fonksiyonu ile yukarı yuvarlamak (yani sonuç 0.5, 0.6 ve hatta 0.1 çıksa bile 1 döndürülür). Ardından bu değere 1 eklemek. Niçin 1 eklediğimizi değişkeni açıklarken anlatmaya çalışmıştım. Ama şimdi sizinle ufak bir simülasyon yaparak konuyu pekiştireceğiz: Diyelim ki ekranımızın genişliği çok küçük. O kadar küçük ki bir gokyuzu sprite’sini bile tamamen göstermeye yetmiyor. Bu durumdayken arkaplanSayisi’nı hesaplayalım: kameranın genişliği bir gokyuzu sprite’sini bile göstermeye yetmiyorsa o zaman kameranın genişliğini gokyuzu sprite’ının genişliğine bölersek 0 ile 1 arasında bir sayı elde ederiz. Mathf.CeilToInt ile bu sayıyı yukarıya yuvarlayıp 1 elde ediyoruz ve ardından sonuca 1 ekliyoruz. Yani bu durumda arkaplanSayisi‘nın değeri 2. Bu da demek oluyor ki oyun başladığında yan yana 2 tane arkaplan objesi oluşturacağız. Peki bu 2 arkaplan objesi bizim hiç kameranın kendi mavi arkaplanını görmememize yetecek mi? Elbette evet! Düşünün hele; kamera 1 arkaplanı bile tam olarak sığdıramıyor ekrana. Bu da demek oluyor ki kamera ikinci arkaplan objesinin ortalarına geldiğinde soldaki arkaplan objesi kameranın dışında kalacak ve ileride düzenleyeceğimiz script vasıtasıyla ikinci arkaplanın tam sağına ışınlanacak. Bu sayede kamera ikinci arkaplanın sonuna ulaşıp biraz daha sağa kaydığında kendi mavi arkaplanını görmek yerine eskiden solda olan ama artık sağa ışınlanmış olan birinci arkaplan objesini görecek. Ve bu döngü böyle devam edecek.

Şimdi bir satır daha aşağı inelim. Burada kameraUnityEbatlar‘a değeri veriliyor. Bu satırı artık açıklamayacağım çünkü şu ana kadar hem kameraUnityEbatlar’ın neyi depoladığından, hem kamera.orthographicSize‘dan hem de camera.orthographicSize’ı kamera.aspect’le çarparsak ne olur defalarca bahsettim.

Start‘ın en altında arkaplanSayisi kadar çalışan bir for döngüsü görüyoruz. Bu for döngüsünde Instantiate komutu ile arkaplan objelerini (gokyuzu ve toprak) oluşturuyoruz. Kameranın transform.position değeri, kameranın tam merkezinin uzaydaki konumunu depolar. Bu değerin X bileşeninden kameraUnityEbatlar.x‘i çıkarırsak kameranın en sol noktasının koordinatını, bu değerin Y bileşenine kameraUnityEbatlar.y‘yi eklersek kameranın en üst noktasının koordinatını buluruz (Y koordinatı aşağıdan yukarıya doğru artar). Kameramızın Y değeri zaten 0 olduğu için, kameraUnityEbatlar.y diyince direkt kameranın en tepe noktasının koordinatını elde ediyoruz.

Hatırlarsanız gokyuzu ve toprak objelerinin Pivot‘larını Top Left yaparak onların position değişkenlerinin en sol üst noktaları için hesaplanmasını sağlamıştık. Ve yine hatırlarsanız kameranın orthographicsSize’ını öyle ayarlamıştık ki kameranın yüksekliği bir gokyuzu ile bir toprak sprite’sini alt alta tam olarak sığdıracak kadardı. O zaman sahnemizdeki gokyuzu sprite’lerinin Y koordinatları ekranın tam tepesine denk gelmeli ve toprak sprite’lerinin Y koordinatları da yerleştirdiğimiz bu gokyuzu sprite’larının tam en alt noktalarının uzaydaki koordinatına denk gelmeli. Bir başka deyişle, kameranın en tepesinin koordinatından gokyuzu’nün yüksekliğinin Uzay Birimi cinsinden dengini çıkarınca elde edeceğimiz sonuç, toprak sprite’larının Y koordinatlarına denk gelmiş olacak.

Peki X koordinatı nasıl hesaplanıyor? Bir kere gokyuzu ve toprak tam alt alta oldukları için ikisinin de X koordinatı aynı olacak, bu konuda hemfikiriz umarım. En soldaki gokyuzu ve toprak ikilisinin X koordinatı, kameranın en sol noktasının koordinatına eşit olacak ve bu da kameranın kendi X koordinatından (position.x) kameraUnityEbatlar.x‘i çıkarınca bulduğumuz değer (aslında kameranın X koordinatı başta 0 olduğu için orada transform.position.x ifadesine gerek yokmuş). Soldan 2. arkaplan objelerini (arkaplan objelerinden kastım her zaman için toprak ve gokyuzu objeleri) nereye ekliyoruz? 1. arkaplan objelerinin hemen sağına. Bu nereye denk geliyor? Ekranın sol noktasının koordinatına gokyuzu’nün yatayda kapladığı Uzay Birimi’ni ekleyince bulduğumuz koordinata denk geliyor. Ve bu obje oluşturma döngüsü böyle devam edip gidiyor (arkaplanSayisi kadar kez).

NOT: Instantiate‘e yazdığım Quaternion.identity‘nin nolduğunu merak ediyor olabilirsiniz. Bir objeyi Instantiate ederken eğim olarak Quaternion.identity verirseniz, o objenin Transform component’indeki Rotation değeri 0,0,0 olur. Bu kadar basit!

Veee bitti! Bu ders boyunca karşılaşacağınız en karmaşık scripti böylece bitirmiş olduk. Umarım hemen her şeyi anlamışsınızdır. Bu kadar çok uğraştık yok Script Birimi‘dir yok Uzay Birimi‘dir yok kameranın orthographicSize‘ıdır ile ama inanın buna değdi. Çünkü artık oyunu hangi ama hangi ekran çözünürlüğünde oynarsak oynayalım arkaplan ekrana hep tam olarak oturacak ve birazdan düzenleyeceğimiz script ile arkaplan sanki hiç bitmiyormuş havası alacağız.

Kameranın Dışında Kalan Arkaplanları En Sağa Işınlamak

Bu bölümde yapacağımız iş de arkaplanla alakalı olduğu için son bölümde yazdığımız Arkaplan.cs scriptinde işlem yapmaya devam edeceğiz. Bu bölümde çok az bir iş yapacağımız için bittiğini anlamayacaksınız bile.

Arkaplan.cs scriptini açın ve içeriğini şöyle güncelleyin:

using UnityEngine;

public class Arkaplan : MonoBehaviour
{
	public GameObject gokyuzu;
	public GameObject toprak;

	private int arkaplanSayisi;

	private Vector2 kameraUnityEbatlar;
	private Vector2 gokyuzuUnityEbatlar;
	private Vector2 toprakUnityEbatlar;

	private Transform[] gokyuzuObjeleri;
	private Transform[] toprakObjeleri;
	private int bastakiArkaplanObjesi = 0;

	void Start()
	{
		Camera kamera = GetComponent<Camera>();

		gokyuzuUnityEbatlar = gokyuzu.GetComponent<SpriteRenderer>().sprite.rect.size / 100;
		toprakUnityEbatlar = toprak.GetComponent<SpriteRenderer>().sprite.rect.size / 100;

		kamera.orthographicSize = ( gokyuzuUnityEbatlar.y + toprakUnityEbatlar.y ) / 2;
		arkaplanSayisi = Mathf.CeilToInt( ( kamera.orthographicSize * 2 * kamera.aspect ) / gokyuzuUnityEbatlar.x ) + 1;

		kameraUnityEbatlar = new Vector2( kamera.orthographicSize * kamera.aspect, kamera.orthographicSize );

		gokyuzuObjeleri = new Transform[arkaplanSayisi];
		toprakObjeleri = new Transform[arkaplanSayisi];

		for( int i = 0; i < arkaplanSayisi; i++ )
		{
			float xKoordinati = transform.position.x - kameraUnityEbatlar.x + i * gokyuzuUnityEbatlar.x;
			gokyuzuObjeleri[i] = Instantiate( gokyuzu, new Vector3( xKoordinati, kameraUnityEbatlar.y, 0 ), Quaternion.identity ).transform;
			toprakObjeleri[i] = Instantiate( toprak, new Vector3( xKoordinati, kameraUnityEbatlar.y - gokyuzuUnityEbatlar.y, 0 ), Quaternion.identity ).transform;
		}
	}

	void Update()
	{
		if( transform.position.x - kameraUnityEbatlar.x >= gokyuzuObjeleri[bastakiArkaplanObjesi].position.x + gokyuzuUnityEbatlar.x )
		{
			gokyuzuObjeleri[bastakiArkaplanObjesi].Translate( arkaplanSayisi * gokyuzuUnityEbatlar.x, 0, 0 );
			toprakObjeleri[bastakiArkaplanObjesi].Translate( arkaplanSayisi * gokyuzuUnityEbatlar.x, 0, 0 );
			bastakiArkaplanObjesi++;

			if( bastakiArkaplanObjesi == gokyuzuObjeleri.Length )
				bastakiArkaplanObjesi = 0;
		}
	}
}

Şimdi scripti kaydedip oyunu çalıştırın. Arkaplan artık hiç bitmeyecektir (daha doğrusu öyle bir izlenim uyandıracaktır.). Dilerseniz oyunu çalıştırdıktan sonra Scene paneline geçiş yapın ve kamera objesini ekrandan takip edin. En soldaki arkaplan objesi kameranın görüş alanından çıktığı anda en sağa ışınlanmakta. Ve inanın bunu yapması hiç de zor değildi. Şimdi siz de göreceksiniz.

Scriptte 3 yeni değişken tanımladım:

gokyuzuObjeleri: Instantiate kullanarak oyunun başında oluşturduğumuz gokyuzu objelerini depolayan bir array (dizi). Fark ettiyseniz türü Transform çünkü gokyuzu objelerini bu şekilde tutunca onların Transform component’inin Translate fonksiyonuna direkt erişebileceğiz. Eğer array’in türü GameObject olsaydı her seferinde transform.Translate demek zorunda kalacaktık. Peh!

toprakObjeleri: Oyunun başında Instantiate kullanarak oluşturduğumuz toprak objelerini depolayan Transform türünde bir array.

bastakiArkaplanObjesi: Bu değer, elimizdeki iki array‘de hangi sıradaki arkaplan objelerinin o anda en solda yer aldığını depoluyor. Oyunun en başında en soldaki arkaplan objeleri arrayin 0. elemanı, onun sağındaki arkaplan objeleri 1. elemanı oluyor ve bu böyle gidiyor. Bu değişkenin ilk değeri de göreceğiniz üzere 0. Yani oyunun en başındayken en soldaki arkaplan objesini temsil ediyor. Ardından diyelim ki kamera biraz ilerledi ve en soldaki arkaplan objeleri en sağa ışınlandı. Artık array’in 1. elemanları (oyun başlarken soldan ikinci sırada olan) en başta oldu. Bu yüzden biz de tam bu anda bastakiArkaplanObjesi değerini artırıp 1 yapıyoruz (birazdan göreceksiniz).

Gelelim Start fonksiyonuna. Burada for döngüsünden hemen önce gokyuzuObjeleri ve toprakObjeleri‘ni initialize ediyorum (yani oluşturuyorum). Bir array’in nasıl oluştuğunu burada görebilirsiniz: gokyuzuObjeleri = new Transform[arkaplanSayisi];

Önce new takısı geliyor, ardından array‘in türü yazılıp köşeli parantezler arasına array’in alacağı eleman sayısı yazılıyor. Böylece işletim sistemi bize hafızadan o kadar elemanı sığdıracak büyüklükte bir yer ayarlıyor ve biz de elemanlarımızı bu hafızaya kaydediyoruz (tabi bu hafıza ile ilgili kısımları hep işletim sistemi hallettiği için biz görmüyoruz bile). Bizim array’lerimizin eleman sayısı arkaplanSayisi kadar çünkü for döngüsü içinde o kadar arkaplan objesi oluşturuyoruz.

for döngüsüne girecek olursak ufacık bir fark göreceğiz. Artık Instantiate ile oluşan arkaplan objelerinin Transform component’lerini .transform değişkeni ile alıyor ve bunu array’lerimizin ilgili elemanına değer olarak atıyoruz.

Update() fonksiyonu scripte yeni geldi. Bu fonksiyon bir tane if‘ten ibaret. Bu if koşulunun içinde kameranın en sol noktasının koordinatının (transform.position.x – kameraUnityEbatlar.x) arkaplan objelerinin en solda yer alanının en sağ noktasının koordinatından (gokyuzuObjeleri[bastakiArkaplanObjesi].position.x + gokyuzuUnityEbatlar.x) büyük olup olmadığına bakıyoruz. Bir başka deyişle, en soldaki arkaplan objesinin kameranın görüş alanının dışına çıkıp çıkmadığına bakıyoruz. Eğer çıkmışsa if‘in içine giriyoruz.

if koşulunun içinde yaptığımız şeyler çok basit: en soldaki gokyuzu ve toprak objelerinin position‘larının X değerini en soldan en sağa ışınlanacak şekilde artırıyoruz. Bunu nasıl yapıyoruz? Çok basit: sahnedeki toplam arkaplan objesi sayısını bir arkaplan objesinin genişliği ile çarpıyoruz ve bu değer kadar soldaki arkaplan objesini x ekseninde Translate ediyoruz. Hemen ardından bastakiArkaplanObjesi değerini 1 artırıyoruz. Yani Unity’e diyoruz ki artık en soldaki arkaplan objesi 0. indexteki değil 1. indexteki oldu (oyunun en başındaki durumu referans alırsak). Çünkü bir satır önce yaptığımız ışınlama sonucu artık 0. indexteki arkaplan objeleri en sağdaki arkaplan objeleri oldu.

Peki diyelim oyunda biraz ilerledik, en soldaki arkaplan objeleri sürekli en sağa ışınlandı ve oyunun başında en solda yer alan arkaplan objesi yine en sola geldi. Şimdi bastakiArkaplanObjesi‘nin değerini geri 0 yapmalıyız. Bunun için de bu değişkenin değerinin gokyuzuObjeleri.Length‘e, yani gokyuzuObjeleri array‘inin uzunluğuna eşit olup olmadığına bakıyoruz. Eğer eşitse anlıyoruz ki array’deki tüm elemanların üzerinden geçmiş ve array’in sonuna gelmişiz. E haliyle bastakiArkaplanObjesi’ni geri sıfırlıyoruz.

Böylece bir başka bölümü daha geride bırakıyoruz.

BONUS – Gökyüzü Arkaplanının Daha Yavaş Sola Kayması (Parallax Scrolling)

Bu bölümde, ekrandaki gokyuzu sprite’larının toprak sprite’larına nazaran daha yavaş sola kaymasını sağlayacağız (çünkü gokyuzu mantıken kameraya toprak’tan daha uzak ve uzaktaki şeyler gerçek hayatta yakındaki şeylere göre daha yavaş görüş alanımızın dışına doğru kayar).

NOT: Bu bölümden vazgeçip vazgeçmemek arasında çok gittim çünkü ne kadar uğraşırsam uğraşayım bu özelliği oyuna ekledikten sonra bazen iki gökyüzü sprite’ı arasında ufacık bir boşluk oluyordu birkaç milisaniyeliğine ve bu boşluk hemen ardından kapanıyordu. Ama bu beni rahatsız etti. Scriptte birkaç deneme yanılma yoluyla en son yaptığım değişiklik sonucu bu boşluk sorunu çok az bir düzeye indi ve ben de bu yüzden bu bölümden vazgeçmemeye karar verdim. Ama eğer isterseniz bu bölümü es geçebilirsiniz, biraz görsellikten kaybedersiniz o kadar. Önemli bir kaybınız olmaz.

Kameranın sağa doğru gitme hızı sabit, yani kamerayı gokyuzu için yavaş, toprak için hızlı bir şekilde sağa kaydıramayız. Peki napacağız? Ufak bir hileye başvuracağız ve gokyuzu sprite’larına biraz sağa doğru yatay hız vereceğiz. Ama bu hız kameranın sağa kayma hızından yavaş olacak, böylece kameranın gokyuzu sprite’larını yakalaması zorlaşacak ve sanki gokyuzu daha yavaş sola kayıyormuş gibi bir izlenim uyanacak bizde.

Hemen şimdi Arkaplan.cs scriptini açın ve içeriğini şöyle güncelleyin:

using UnityEngine;

public class Arkaplan : MonoBehaviour
{
	public GameObject gokyuzu;
	public GameObject toprak;

	public float gokyuzuSagaKaymaHizi = 1.0f;

	private int arkaplanSayisi;

	private Vector2 kameraUnityEbatlar;
	private Vector2 gokyuzuUnityEbatlar;
	private Vector2 toprakUnityEbatlar;

	private Transform[] gokyuzuObjeleri;
	private Transform[] toprakObjeleri;
	private int bastakiGokyuzuArkaplanObjesi = 0;
	private int bastakiToprakArkaplanObjesi = 0;

	private Transform gokyuzuParent;

	void Start()
	{
		Camera kamera = GetComponent<Camera>();

		gokyuzuUnityEbatlar = gokyuzu.GetComponent<SpriteRenderer>().sprite.rect.size / 100;
		toprakUnityEbatlar = toprak.GetComponent<SpriteRenderer>().sprite.rect.size / 100;

		kamera.orthographicSize = ( gokyuzuUnityEbatlar.y + toprakUnityEbatlar.y ) / 2;
		arkaplanSayisi = Mathf.CeilToInt( ( kamera.orthographicSize * 2 * kamera.aspect ) / gokyuzuUnityEbatlar.x ) + 1;

		kameraUnityEbatlar = new Vector2( kamera.orthographicSize * kamera.aspect, kamera.orthographicSize );

		gokyuzuObjeleri = new Transform[arkaplanSayisi];
		toprakObjeleri = new Transform[arkaplanSayisi];

		gokyuzuParent = new GameObject().transform;

		for( int i = 0; i < arkaplanSayisi; i++ )
		{
			float xKoordinati = transform.position.x - kameraUnityEbatlar.x + i * gokyuzuUnityEbatlar.x;
			gokyuzuObjeleri[i] = Instantiate( gokyuzu, new Vector3( xKoordinati, kameraUnityEbatlar.y, 0 ), Quaternion.identity ).transform;
			gokyuzuObjeleri[i].SetParent( gokyuzuParent );
			toprakObjeleri[i] = Instantiate( toprak, new Vector3( xKoordinati, kameraUnityEbatlar.y - gokyuzuUnityEbatlar.y, 0 ), Quaternion.identity ).transform;
		}
	}

	void Update()
	{
		if( transform.position.x - kameraUnityEbatlar.x >= gokyuzuObjeleri[bastakiGokyuzuArkaplanObjesi].position.x + gokyuzuUnityEbatlar.x )
		{
			gokyuzuObjeleri[bastakiGokyuzuArkaplanObjesi].Translate( arkaplanSayisi * gokyuzuUnityEbatlar.x, 0, 0 );
			bastakiGokyuzuArkaplanObjesi++;

			if( bastakiGokyuzuArkaplanObjesi == gokyuzuObjeleri.Length )
				bastakiGokyuzuArkaplanObjesi = 0;
		}

		if( transform.position.x - kameraUnityEbatlar.x >= toprakObjeleri[bastakiToprakArkaplanObjesi].position.x + toprakUnityEbatlar.x )
		{
			toprakObjeleri[bastakiToprakArkaplanObjesi].Translate( arkaplanSayisi * gokyuzuUnityEbatlar.x, 0, 0 );
			bastakiToprakArkaplanObjesi++;

			if( bastakiToprakArkaplanObjesi == gokyuzuObjeleri.Length )
				bastakiToprakArkaplanObjesi = 0;
		}

		gokyuzuParent.Translate( gokyuzuSagaKaymaHizi * Time.deltaTime, 0, 0 );
	}
}

Gelelim scriptte neler nelerin yenilendiğine. Maalesef ki gökyüzünün daha yavaş sola kaymasını sağlamak düşündüğümden daha zor oldu ama sonuçta ortaya daha güzel bir görüntü çıktı (bence).

gokyuzuSagaKaymaHizi: Scripte yeni eklenen değişkenlerden biri. Bu değişkenin değerini ne kadar artırırsanız, gokyuzu objeleri sahnede (Scene paneli) o kadar hızlı sağa kadar ve haliyle Game panelinde o kadar yavaş sola kayıyor izlenimi uyandırır. Yalnız eğer değişkenin değeri kusHareket scriptindeki yatayHiz değişkeninin değerini geçerse, bu sefer arkaplan gerçek anlamda sağa kaymaya başlar (Game panelinde de sağa kayar).

bastakiGokyuzuArkaplanObjesi ve bastakiToprakArkaplanObjesi: Eskiden bu ikisinin değeri aynıydı ve bastakiArkaplanObjesi değişkeninde tutuluyordu. Ama artık gökyüzü daha yavaş sola kaydığı için gökyüzü arkaplanı objelerinin kameranın solundan dışına çıkıp sağına ışınlanması daha uzun sürecek ve haliyle bir süre sonra en soldaki gokyuzu objesinin array‘deki sırası (index) ile en soldaki toprak objesinin array’deki sırası (index) farklı olacak. Bu yüzden bastakiArkaplanObjesi değişkenini böyle ikiye ayırdık.

gokyuzuParent: Bu değişken, içerisinde bir Empty GameObject depolayacak ve bu Empty GameObject sahnedeki tüm gokyuzu objelerinin parent‘ı olacak. Bu sayede tüm gokyuzu objelerini tek tek sağa oynatmak yerine sadece gokyuzuParent objesini sağa oynatmamız yetecek (unutmayın; A objesi B objesinin parent objesi ise, A objesi ne yöne hareket ederse B objesi de otomatik olarak o yöne hareket eder.). Parent obje kullanmanın bir başka avantajı da bir şekilde bu yöntemin ekranda oluşan boşlukların (bölümün başında bahsettiğim şey) sayısını çok aza indirgemesi. Sebebini bilmiyorum, ben de deneme-yanılma yoluyla bu yöntemin iyi olduğunu fark ettim.

Şimdi Start fonksiyonunun içine girelim. Burada “gokyuzuParent = new GameObject().transform;” kodunu görebilirsiniz. gokyuzuParent‘ın bir Empty GameObject olduğundan bahsetmiştim. Ama bu Empty GameObject’in oyunun başında script vasıtasıyla oluşturulduğundan bahsetmedim. Bu işlemi yapmak için ise (oyun sırasında sahnede Empty GameObject oluşturmak) Unity’nin şu fonksiyonunu kullanıyoruz: new GameObject(). Bu fonksiyon sahnede 0,0,0 position koordinatlarında yeni bir Empty GameObject oluşturup onu bize döndürüyor. Ben de hemen ardından bu GameObject’in Transform component’ine erişiyor ve onu gokyuzuParent‘a atıyorum (gokyuzuParent’ın türünün Transform olduğuna dikkat edin).

Start fonksiyonundaki son değişiklik ise, for döngüsünün içindeki yeni bir kod satırı: “gokyuzuObjeleri[i].SetParent( gokyuzuParent );“. Bu kod sayesinde oluşturduğumuz gokyuzu objelerine parent olarak gokyuzuParent objesini atıyoruz. İşte bu kadar basit!

Update fonksiyonuna gelelim. Hatırlarsanız burada eskiden sadece bir if koşulu vardı ve o da en soldaki gokyuzu objesinin kameranın görüş alanından çıkıp çıkmadığına bakıyordu. Eğer çıkmışsa en soldaki gokyuzu ve toprak objelerini en sağa ışınlıyordu (gokyuzu ve toprak o sıralar aynı hızda hareket ettikleri için biri dışarı çıkarsa bu ötekinin de dışarı çıktığı anlamına geliyordu). Artık gokyuzu ve toprak arkaplan objelerinin sola kayma hızları farklı olduğu için en soldaki gokyuzu ve en soldaki toprak objelerinin kameranın dışına çıkıp çıkmadıklarını ayrı ayrı kontrol etmeliyiz. Bu yüzden de Update‘teki tek if koşulunu ikiye ayırdım. İlk if koşulu en soldaki gokyuzu objesinin, ikinci if koşulu da en soldaki toprak objesinin kameranın dışına çıkıp çıkmadığını kontrol ediyor.

Update‘in en altına ise yeni bir kod ekledik. Bu kodun tek yaptığı şey, gokyuzuParent objesini (haliyle bu objenin child objesi olan tüm gokyuzu objelerini) yatay koordinat düzleminde saniyede (Time.deltaTime kullandığıma dikkat edin) gokyuzuSagaKaymaHizi kadar sağa kaydırmak. Hiç de karışık değilmiş, öyle değil mi!

Böylece bir başka bölümün daha sonuna varmış bulunmaktayız.

Engelleri Rastgele Şekilde Oluşturmak

Bu bölümde oyuna meşhur engellerimizi ekleyeceğiz. Engellerin yüksekliği rastgele olarak ayarlanacak ve kameranın dışında kalan engeller, tıpkı arkaplan objelerinde olduğu gibi en sağa ışınlanacak. Bu da demek oluyor ki oyun boyunca kullanacağımız tüm engelleri de arkaplan objeleri gibi oyunun en başında oluşturacağız.

Bu iş için Engeller adında yeni bir C# script oluşturup bunu da Main Camera‘ya atayın. Hatırlarsanız gokyuzu ve toprak objelerinin Pivot‘larını Top Left yaparak position değerlerinin en sol üst noktalarını temsil etmesini sağlamıştık. Şimdi benzer şekilde altEngel sprite’ı için de Pivot’u Top Left yapın (Apply demeyi unutmayın) ama bu sefer ustEngel objesi için Pivot’u Bottom Left yapın:

resim11

Neden böyle yaptık hemen açıklayayım. Yeni yazacağımız scriptimizde engelleri oluştururken kullandığımız algoritma şöyle: kameranın üst noktasının koordinatı ile alt noktasının koordinatı arasında rastgele bir koordinat seçiyoruz ve bu koordinatı üst ve alt engel sprite’leri arasındaki boşluğun sol üst noktası olarak kabul ediyoruz (bu noktaya yKoordinati diyelim). Eğer ki ustEngel‘in Pivot‘u da Top Left olsaydı, üst engeli yerleştirirken engelin Y koordinatını yKoordinati + ustEngel sprite’ının yüksekliğinin denk geldiği Uzay Birimi şeklinde yapacaktık. Böylece ustEngel’in en alt noktası bu boşluğun başlangıcına denk gelecekti. Ama artık Pivot’u Bottom Left yaptığımız için, ustEngel’in transform.position değeri sprite’ının en sol alt noktasını temsil edecek. Yani objeyi direkt yKoordinati‘na yerleştirmek işimizi görecek.

Bu kısacık açıklamanın ardından scripti açın ve içeriğini şöyle düzenleyin:

using UnityEngine;

public class Engeller : MonoBehaviour
{
	public GameObject ustEngel;
	public GameObject altEngel;

	public float altUstEngelArasiBosluk = 0.8f;
	public float ikiEngelArasiMesafe = 2f;

	private int engelSayisi;

	private Vector2 kameraUnityEbatlar;
	private Vector2 engelUnityEbatlar;

	private Transform[] ustEngelObjeleri;
	private Transform[] altEngelObjeleri;
	private int bastakiEngelObjesi = 0;

	void Start()
	{
		Camera kamera = GetComponent<Camera>();

		engelUnityEbatlar = ustEngel.GetComponent<SpriteRenderer>().sprite.rect.size / 100;
		kameraUnityEbatlar = new Vector2( kamera.orthographicSize * kamera.aspect, kamera.orthographicSize );

		engelSayisi = Mathf.CeilToInt( ( kamera.orthographicSize * 2 * kamera.aspect ) / ( engelUnityEbatlar.x + ikiEngelArasiMesafe ) ) + 1;

		ustEngelObjeleri = new Transform[engelSayisi];
		altEngelObjeleri = new Transform[engelSayisi];

		for( int i = 0; i < engelSayisi; i++ )
		{
			float xKoordinati = transform.position.x + kameraUnityEbatlar.x + i * ( engelUnityEbatlar.x + ikiEngelArasiMesafe );
			float yKoordinati = Random.Range( -kameraUnityEbatlar.y + altUstEngelArasiBosluk + 0.6f, kameraUnityEbatlar.y - 0.6f );
			ustEngelObjeleri[i] = Instantiate( ustEngel, new Vector3( xKoordinati, yKoordinati, 0 ), Quaternion.identity ).transform;
			altEngelObjeleri[i] = Instantiate( altEngel, new Vector3( xKoordinati, yKoordinati - altUstEngelArasiBosluk, 0 ), Quaternion.identity ).transform;
		}
	}

	void Update()
	{
		if( transform.position.x - kameraUnityEbatlar.x >= ustEngelObjeleri[bastakiEngelObjesi].position.x + engelUnityEbatlar.x )
		{
			float yKoordinati = Random.Range( -kameraUnityEbatlar.y + altUstEngelArasiBosluk + 0.6f, kameraUnityEbatlar.y - 0.6f );

			Vector3 ustEngelPosition = ustEngelObjeleri[bastakiEngelObjesi].position;
			Vector3 altEngelPosition = altEngelObjeleri[bastakiEngelObjesi].position;

			ustEngelPosition.x += engelSayisi * ( engelUnityEbatlar.x + ikiEngelArasiMesafe );
			altEngelPosition.x += engelSayisi * ( engelUnityEbatlar.x + ikiEngelArasiMesafe );

			ustEngelPosition.y = yKoordinati;
			altEngelPosition.y = yKoordinati - altUstEngelArasiBosluk;

			ustEngelObjeleri[bastakiEngelObjesi].position = ustEngelPosition;
			altEngelObjeleri[bastakiEngelObjesi].position = altEngelPosition;

			bastakiEngelObjesi++;

			if( bastakiEngelObjesi == ustEngelObjeleri.Length )
				bastakiEngelObjesi = 0;
		}
	}
}

Tam bu noktada ufak ayarlamaları ve script’i tanıtmayı yapmadan önce bir şey yapmamız lazım: Arkaplan.cs scriptini geri açın ve Start fonksiyonunu Awake fonksiyonu olarak değiştirin (void Start() satırını void Awake() olarak değiştirin). Bunu yapmanız çok önemli çünkü Awake fonksiyonu her zaman Start‘tan önce çalışır ve bizim oyunumuzda Arkaplan.cs scriptinin Engeller.cs scriptinden önce çalışması lazım. Peki neden? Çünkü Engeller.cs scriptinde de kameraUnityEbatlar diye bir değişken var ve bu değişkenin değerini düzgün alması için, önce Arkaplan.cs scriptindeki kamera.orthographicsSize = blabla; satırının çalışması gerekiyor.

Şimdi ustEngel ve altEngel‘i birer prefab‘a çevirip Main Camera‘daki Engeller (Script) component’ine parametre olarak atamamız lazım. Nasıl gokyuzu ve toprak’i prefab yaptıysak bunları da aynı şekilde prefab yapıyoruz. Sırayla Scene paneline sürükleyip Transform component’indeki değerleri resetliyoruz. Sprite Renderer‘larındaki Order in Layer‘ı -7 yapıyoruz (sprite’ların ekranda çizilme sırası Gökyüzü->Engel->Toprak->FlappyBird oldu). Ardından objeyi Hierarchy‘den tutup sürükleyerek Project paneline bırakıyoruz. Prefablarımız oluşunca Hierarchy’deki ustEngel ve altEngel’i siliyoruz (artık onlara gerek kalmadı) ve oluşan prefablarımızı projenin düzenli durması için Prefablar klasörüne atıyoruz.

Artık prefab’larımız hazır olduğuna göre onları Engeller (Script) component’indeki yerlerine sürükleyin ve oyunu çalıştırın.

Oyun alanında rastgele yükseklikte engeller çıkacaktır. Siz ilerledikçe yeni engeller çıkmaya devam edecek. Ama henüz engele çarpsanız da bir şey olmuyor. Bu aşamada önemli olan engellerin oluşması.

resim12

Yazdığımız scriptin Arkaplan.cs scriptine benzediğine dikkat ettiniz mi? Değişken isimlerinden tutun kodun içeriğine kadar çok benzer yönleri var. Bakalım scriptimizde neler varmış:

ustEngel ve altEngel: Engel prefab‘larını depolayıp Instantiate metoduyla oyunun başında engelleri oluşturmakta kullanılan değişkenler.

altUstEngelArasiBosluk: ustEngel ile altEngel sprite’ları arasında kaç Uzay Birimi kadar boşluk olması gerektiğini belirten değer. Değerini Inspector‘dan artırırsanız engellerin arası açılır, oyun kolaylaşır.

ikiEngelArasiMesafe: Bir engel oluşturulduktan sonra, ardından gelen engelin (engelden kastım ustEngel ve altEngel’in bir arada oluşturmuş olduğu engel) ne kadar Uzay Birimi sonra geleceğini belirler. Eğer değerini Inspector‘dan artırırsanız iki engel arası uzaklık artar, bir engel geçtikten sonra ötekinin gelmesi için daha çok beklersiniz. Yani oyun kolaylaşır.

engelSayisi: Nasıl Arkaplan.cs‘de arkaplanSayisi varsa bu scriptte de engelSayisi var. Oyunun başında kaç tane engel oluşturursak bu engellerin oyun boyunca bize yeteceğini depoluyor kendisi.

kameraUnityEbatlar: Arkaplan.cs‘deki aynı isimli değişkenin değeriyle birebir aynı değeri alıyor.

engelUnityEbatlar: Bir ustEngel sprite’ının yatayda ve dikeyde kaç Uzay Birimi ebatlarında olduğunu depolar. ustEngel ile altEngel’in ebatları aynı olduğu için, aynı zamanda altEngel’in de ebatlarını depolamış olur.

ustEngelObjeleri ve altEngelObjeleri: Sahnede oluşturulan ustEngel objelerini ve altEngel objelerini depolayan array‘ler.

bastakiEngelObjesi: Ekranımızda görünür vaziyetteki engellerden en soldakinin ustEngelObjeleri ve altEngelObjeleri array‘lerinde kaçıncı index‘te yer aldığını depolar.

Start fonksiyonunda ilk satırlarda engelUnityEbatlar‘a ve kameraUnityEbatlar‘a değerlerini veriyoruz. Burada kameraUnityEbatlar’ın değerini kamera.orthographicSize‘ı kullanarak aldığına dikkat edin. Arkaplan.cs scriptinde de böyleydi ama öncesinde camera.orthographicSize’ın değerini değiştiriyorduk. İşte Arkaplan.cs’deki Start fonksiyonunu Awake ile değiştirmemizin sebebi de tam burada yatıyor. Önce Arkaplan.cs’de kamera.orthographicSize’ın belirlenmesini sağlıyoruz. Böylece artık Engeller.cs scriptinde kameraUnityEbatlar‘a değerini verirken, kamera.orthographicSize‘ın değerinin güncel değer olduğundan emin oluyoruz.

Bu scriptte engelSayisi‘nı Arkaplan.cs‘deki arkaplanSayisi‘na çok benzer bir şekilde buluyoruz. Tek fark şu: arkaplanSayisi’nda bir arkaplanın genişliğini gokyuzuUnityEbatlar.x olarak alırken burada bir engelin genişliğini “engelUnityEbatlar.x + ikiEngelArasiMesafe” olarak alıyoruz. Böyle yapıyoruz zira her engelden sonra ikiEngelArasiMesafe kadar da bir boşluk olmasını istiyoruz.

Gelelim for döngüsüne. Oyun başlarken engellerin kameranın en sağından başlamasını istiyoruz, o yüzden xKoordinatı transform.position.x – kameraUnityEbatlar.x‘ten değil de transform.position.x + kameraUnityEbatlar.x‘ten başlıyor. Böylece ilk engelin X koordinatını kameranın en sağ noktası olarak belirliyoruz. Burada ekstradan yKoordinati diye bir şey var. Bildiğiniz üzere engelleri yerleştirirken ustEngel ile altEngel arasında boşluk olmasını istiyoruz (ki kuş bu aralıktan geçebilsin, değil mi). İşte yKoordinati tam olarak bu boşluğun tepe noktasının Y koordinatına denk geliyor. Bu da demek oluyor ki bu boşluğun en alt noktasının Y koordinatını bulmak istersek, tek yapmamız gereken yKoordinati – altUstEngelArasiBosluk işlemini çözmek (Y koordinatı yukarıdan aşağı indikçe azalır). Hatırlarsanız ustEngel‘in Pivot noktasını Bottom Left yaptık, yani engelin Inspector‘daki Position değeri bize sol alt noktasının koordinatını veriyor. Benzer şekilde altEngel‘in Pivot‘unu da Top Left yapmıştık. Yani ustEngel’in position değeri sol üst noktasını temsil ediyor. O halde yapmamız gereken şey, ustEngel’i tam yKoordinati‘na, altEngel’i de yKoordinati – altUstEngelArasiBosluk noktasına koymak. Instantiate komutlarına bakarsanız da tam olarak bunu yaptığımızı göreceksiniz (açıklama resimden sonra devam ediyor).

resim13

Peki yKoordinati‘nı nasıl bulduk? Hepimiz biliyoruz ki Flappy Bird’de engellerin yüksekliği rastgele belirleniyor. Bizim oyunumuzda da bu durum geçerli. Random.Range ile belli bir aralıkta rastgele bir float döndürülüyor ve bu değer yKoordinati‘na atanıyor. Peki bu “belli aralık” ne? Bu belli aralığın bir minimum ve maksimum değeri var. Döndürülen sayı da bu aralığın içinden seçiliyor. Aralığın minimum değerini bulmak için Random.Range’in ilk parametresine bakmanız yeterli: “-kameraUnityEbatlar.y + altUstEngelArasiBosluk + 0.6“. Bildiğiniz gibi -kameraUnityEbatlar.y, kameranın görüş alanının en alt noktasının koordinatını veriyor. Bizim yKoordinati‘mız boşluğun tepe koordinatını depoluyordu. Bu yüzden bu değere altUstEngelArasiBosluk‘u ekliyoruz ve engeller arası boşluğun bulunabileceği en dip noktanın koordinatını buluyoruz. Ama bu değere bir de 0.6 Uzay Birimi ekliyoruz. Eklemezsek ne olur? Eğer rastgele sayımız tam “-kameraUnityEbatlar.y + altUstEngelArasiBosluk” olarak döndürülürse boşluk kameranın en dibinde yer alır ve ekranda sadece ustEngel gözükür. altEngel kameranın dışında kalır. Bunu engellemek için boşluğun kameranın dibinden en azından 0.6 Uzay Birimi kadar uzak olduğundan emin oluyoruz. Dilerseniz bu değeri kendiniz de değiştirebilirsiniz. Ne kadar artırırsanız engeller arası boşluğun kameranın en alt noktasından o kadar uzakta başlamasını garantilersiniz.

Fark etmiş olabilirsiniz, Start fonksiyonunda çoğu şeyi anlatmadım çünkü yaptığımız şeyler Arkaplan.cs ile neredeyse birebir aynıydı. Sadece önemli noktaların üzerinden geçtim. Aynı şeyi şimdi Update için yapacağım. Update’in en başında en soldaki engel objesinin kameranın görüş alanından çıkıp çıkmadığına bakıyoruz. Eğer çıkmışsa yeni bir rastgele yKoordinati buluyoruz (böylece engel sağa ışınlandığında farklı bir yüksekliğe sahip olacak). Ardından en soldaki ustEngel ile altEngel‘i en sağa ışınlayıp position‘larının Y değerini uygun şekilde değiştiriyoruz.

Bu bölümle beraber oyunun büyük çoğunluğunu bitirdik. Şu haliyle bile istediğiniz zorlukta bir Flappy Bird oyunu oluşturmak için pek çok değişken emrinizde: KusHareket.cs scriptindeki yercekimi, ziplamaGucu ve yatayHiz; Engeller.cs scriptindeki altUstEngelArasiBosluk ve ikiEngelArasiMesafe.

Kuş Engele Çarpınca Oyunun Bitmesi

Bu bölümde yapacağımız tek şey şu olacak: kuşun bir engele çarpıp çarpmadığını kontrol edeceğiz ve eğer çarpmışsa kuşun artık sağa gitmesini engelleyip toprağa düşmesini sağlayacağız. Kuş toprağa düştükten sonra oyuncu ekrana tıklarsa oyunu yeniden başlatacağız.

Unity’de iki objenin temas edip etmediğini test etmeye yarayan bir fonksiyon var: OnCollisionEnter(). Bu fonksiyonun çalışması için ise hem temas eden objede hem de temas edilen objede Collider adında bir component olması gerekiyor. Buna ek olarak, temas eden objede Rigidbody component’i de olması gerekiyor. Temas eden obje burada FlappyBird olurken temas edilen obje ise ustEngel veya altEngel oluyor.

İşe FlappyBird objesine Collider2D ve Rigidbody2D ekleyerek başlayalım. Bunun için kuşu seçin ve yukarıdan Component-Physics 2D-Rigidbody 2D yolunu izleyin. Ardından Component-Physics 2D-Circle Collider 2D yolunu izleyin.

İlk önce kuşa Unity’nin 2D motorunda çalışan Rigidbody versiyonunu, Rigidbody 2D‘yi ekledik. Ardından Circle Collider 2D ile kuşun başka objelerle temas eden alanını bir daire olarak belirledik. Bu temas alanını Scene panelinde kuşu çevreleyen yeşil bir dairesel hat olarak görebilirsiniz. Bence bu alan biraz büyük, bu yüzden bunu küçülttüm ben. Bunun için Inspector’dan Circle Collider 2D component’i altında yer alan Radius (yarıçap) değerini 0.115 olarak değiştirdim. Kuşu çevreleyen hat haliyle küçüldü:

resim14

Unity’nin 2D motorunda şu anda ufak bir bug var: eğer Rigidbody 2D‘de Body Type‘ı Kinematic yaparsak obje başka objelerle temas etmiyor. Ama eğer yapmazsak da varsayılan olarak obje yerçekiminden etkileniyor. Bizse kuşun yerçekiminden etkilenmesini istemiyoruz çünkü kuşun tüm hareket işlerini kod vasıtasıyla kendimiz hallediyoruz. Bu sorunu çözmek ise kolay: Rigidbody 2D’deki Gravity Scale değerini 0 yapmanız yeterli. Artık yerçekimi kuşa etki etmeyecek. Buna ek olarak Linear Drag ve Angular Drag değerlerini de 0 yapın. Ne işe yaradıklarını bilmiyorum ama Rigidbody 2D’mizin, kuşun başka objelerle temas etmesini sağlamak dışında bir iş yapmadığından emin olalım biz.

Şimdi kuşun temas edebileceği objelere, yani ustEngel‘e, altEngel‘e ve toprak‘a da birer Collider 2D ekleyelim. Başlangıcı ustEngel ile yapalım. Şu anda herhangi bir ustEngel objesi sahnede yer almadığından ustEngel’e ekleyeceğimiz Collider’ın dış hattını gözle görerek değiştiremeyiz. Sahnede referans bir ustEngel objesi lazım bize. Bunun için Project panelinden ustEngel prefabını seçip sürükleyerek Scene paneline bırakın. Referansımız hazır:

resim15

Şimdi sahneye eklediğiniz ustEngel seçiliyken, yukarıdan Component-Physics 2D-Box Collider 2D yolunu izleyin. Bu, dikdörtgen şeklinde bir temas alanı oluşturmak için kullanılır. ustEngel’in çevresinde yeşil bir dikdörtgen hat göremeyebilirsiniz çünkü o hat, objeyi saran gri hatla çakışıyor, gri hattın gerisinde kalıyor.

Bu safhada bir şeyi daha değiştirmemiz gerekiyor. Şu anda ustEngel’in Box Collider 2D component’inde Is Trigger seçili değil. Bunun anlamı şu: kuş objemiz ustEngel ile temas edince onun içinden geçemeyecek. Aslında bizim istediğimiz de bu gibi duruyor ama değil. Bizim istediğimiz, kuşun bir engele çarpınca yatay eksende hareket etmeyi kesip toprağa düşmesi ve toprakta hareketinin tamamen son bulması (dikey eksende de hareketinin kesilmesi). Bu yüzden kuş bir engele çarpınca toprağa düşene kadar diğer engellerin içinden geçebilmeli. Bunu sağlamak için de Collider’da Is Trigger seçeneğini işaretleyin.

Engel objemize collider ekledik. Şimdi bu collider’ı Project panelindeki prefab’a uyarlayalım ki Instantiate metoduyla oluşturduğumuz tüm ustEngel objelerinde collider yer alsın. Bunu yapmak çok kolay. Sahnedeki ustEngel seçili iken Inspector‘dan Apply butonuna basın. Bunu yaptığınızda objedeki tüm değişiklikler onu oluşturan prefab‘a da uygulanır.

resim16

Artık sahnedeki ustEngel‘e ihtiyacımız kalmadığından onu silebilirsiniz. Sonrasında ise altEngel ve toprak prefab‘larını da ayrı ayrı sahneye taşıyıp Box Collider 2D verin (altEngel’de Is Trigger‘ı işaretleyin ama toprak’ta işaretlemeyin, çünkü kuşun hareketinin toprak’a çarpınca tamamen son bulmasını istiyoruz) ve değişiklikleri prefab‘larına uygulayın. Artık teması tetikleyen tüm component’ler objelerimizde yer almakta.

Şimdi kod yazma zamanı. Kuşun temas olaylarını ayarlamak için yeni bir script oluşturmamıza gerek yok. KusHareket.cs scriptini açın ve en alta şu iki fonksiyonu ekleyin:

void OnTriggerEnter2D( Collider2D temas )
{
	Destroy( Camera.main.GetComponent<Arkaplan>() );
	Destroy( GetComponent<KusAnimasyon>() );
	yatayHiz = 0;
}

void OnCollisionEnter2D( Collision2D temas )
{
	Destroy( Camera.main.GetComponent<Arkaplan>() );
	Destroy( GetComponent<KusAnimasyon>() );
	yatayHiz = 0;
}

OnTriggerEnter2D: Obje, collider‘ında Is Trigger işaretli bir objeyle temas edince çalıştırılır.

OnCollisionEnter2D: Obje, collider‘ında Is Trigger işaretli olmayan bir objeyle temas edince çalıştırılır.

Kuş engele çarpınca yaptığımız işlem çok basit: Önce Arkaplan.cs scriptini kameradan çıkarıyoruz ve böylece gokyuzu‘nün sağa doğru kaymasını sonlandırmış oluyoruz. Bununla beraber FlappyBird‘deki KusAnimasyon component’ini de siliyoruz ve böylece kuş artık kanat çırpmıyor. Ardından yatayHiz‘ı sıfırlıyoruz ve böylece artık kuş yatay eksende hareket etmeyi sonlandırıyor. Peki neden içinde birebir aynı kod olan iki ayrı fonksiyon kullandık? Çünkü OnTriggerEnter2D kuş ustEngel ve altEngel‘e çarpınca, OnCollisionEnter2D ise sadece kuş toprak‘a çarpınca gerçekleştiriliyor. Kuş engele çarpınca zaten yatayHiz’ı OnTriggerEnter2D’de sıfırlanıyor, niye bir de OnCollisionEnter2D’de sıfırlanıyor diyebilirsiniz. Bunun sebebi ise, kuşun hiçbir engele çarpmadan direkt toprağa çarparak da ölmesinin mümkün olması.

Scripti kaydedip oyunu test ederseniz bir gariplik fark edeceksiniz: Kuş toprağa çarpınca sapıtıyor, olduğu yerde kalmıyor. Çünkü kuştaki Rigidbody 2D ve Collider 2D, kuşu temas ettiği yerde tutmaya çalışırken KusHareket.cs scriptinin Update fonksiyonundaki Translate fonksiyonu kuşa yerçekimi uyguluyor ve bu iki güç birbiriyle çatışma haline giriyor. Ama neticede yerçekimi yeniyor ve kuş ekranımızdan çıkıyor. Buna engel olmalıyız. Ve inanın çözümü çok basit. Tek yapmamız gereken kuş toprağa çarpınca artık KusHareket.cs scriptindeki Update fonksiyonunun çalışmasını engellemek, böylece artık Translate komutu çalıştırılmayacak. KusHareket.cs scriptini geri açın ve scripti şöyle güncelleyin:

using UnityEngine;

public class KusHareket : MonoBehaviour
{
	public float yercekimi = 4f;
	public float ziplamaGucu = 2.5f;
	public float yatayHiz = 1.5f;
	private float dikeyHiz = 0f;

	private bool oyunBitti = false;

	void Update()
	{
		if( !oyunBitti )
		{
			dikeyHiz -= yercekimi * Time.deltaTime;

			if( Input.GetMouseButtonDown( 0 ) )
				dikeyHiz = ziplamaGucu;

			float egim = 90 * dikeyHiz / yatayHiz;

			if( egim < -50 ) egim = -50;
			else if( egim > 50 ) egim = 50;

			transform.eulerAngles = new Vector3( transform.eulerAngles.x, transform.eulerAngles.y, egim );
			transform.Translate( new Vector3( 0, dikeyHiz, 0 ) * Time.deltaTime, Space.World );
			transform.parent.Translate( yatayHiz * Time.deltaTime, 0, 0 );
		}
	}

	void OnTriggerEnter2D( Collider2D temas )
	{
		Destroy( Camera.main.GetComponent<Arkaplan>() );
		Destroy( GetComponent<KusAnimasyon>() );
		yatayHiz = 0;
	}

	void OnCollisionEnter2D( Collision2D temas )
	{
		Destroy( Camera.main.GetComponent<Arkaplan>() );
		Destroy( GetComponent<KusAnimasyon>() );
		yatayHiz = 0;
		oyunBitti = true;
	}
}

Scriptin en başında oyunBitti adında bir değişken tanımladık, varsayılan değeri false. Kuş toprağa değince (OnCollisionEnter2D) bu değişkeni true yapıyoruz. Ardından Update fonksiyonunun hepsini kaplayan if( !oyunBitti ) koşulumuz artık sağlanmayacağı için de, Update fonksiyonunun içindeki kodların hiçbiri artık çalıştırılmıyor.

Scripti kaydedip test edin. Sonuç tam istediğimiz gibi oldu. Bense tam bu noktada çok önemli bir bug’ı atladığımızı keşfettim. Eğer bir engele çarptıktan sonra kuş toprağa değmeden mouse ile ekrana tıklarsanız kuş zıplamaya devam ediyor. Bunun için en uygun çözümün kuş bir engele çarpınca başka bir bool değişkenin değerini true yapmak ve bu değişken true ise kuşun kanat çırpmasını engellemek olduğunu düşünüyorum. Yapmanız gereken şey, scriptin başında “private bool engeleCarptim = false;” değişkenini tanımlamak ve Update fonksiyonunda şu değişikliği yapmak:

if( Input.GetMouseButtonDown( 0 ) && !engeleCarptim )
	dikeyHiz = ziplamaGucu;

Eğer oyuncu ekrana tıklarsa engeleCarptim değişkeninin false olup olmadığına bakıyoruz ve false ise kuşu zıplatıyoruz.

Buna ek olarak, OnTriggerEnter2D‘nin en sonuna da “engeleCarptim = true;” komutunu ekleyin ve oyunu test edin. Artık bu sorundan kurtulmuş olmanız lazım.

Scripte bir de oyunu öldükten sonra yeniden başlatma kodu eklersek tam olacak. Bunun için Update fonksiyonunu şöyle güncelleyin:

if( !oyunBitti )
{
	...
}
else
{
	if( Input.GetMouseButtonDown( 0 ) )
		UnityEngine.SceneManagement.SceneManager.LoadScene( "Oyun" );
}

Eğer kuş toprağa çarpmışsa (oyunBitti true ise) ve oyuncu ekrana tıklamışsa, Oyun sahnemizi (scene) yeniden yüklüyoruz, yani restart atıyoruz levele. Hiç de zor değilmiş.

Kuş Kameranın Dışına Çıkınca Ölmesini Sağlamak

Bilirsiniz, normal Flappy Bird’de kuşu çok zıplatıp kameranın tepesine çarptırırsanız oyun biter. Biz de bunu yapacağız bu bölümde. Sandığınızdan da kısa sürecek!

Tüm işi KusHareket.cs scriptinde halledeceğiz. Scripti açıp başında şu değişkeni tanımlayın: “private Vector2 kameraUnityEbatlar;“. Sonra tıpkı Engeller.cs scriptindeki gibi, Start fonksiyonuna şu satırı ekleyin (daha doğrusu KusHareket.cs‘de henüz Start olmadığı için, şu şekilde Start fonksiyonu oluşturun):

private void Start()
{
	Camera kamera = Camera.main;
	kameraUnityEbatlar = new Vector2( kamera.orthographicSize * kamera.aspect, kamera.orthographicSize );
}

Hatırlarsanız kameraUnityEbatlar.y bize kameranın en üst noktasının koordinatını veriyordu. Eğer kuşun Y koordinatı bu koordinattan büyük ise, kuş ekranın dışına çıkmış demektir. Bu durumda kuşu öldüreceğiz. Update fonksiyonunu açıp “if( !oyunBitti )” koşulunun içinde istediğiniz yere şu kodu ekleyin:

if( transform.position.y > kameraUnityEbatlar.y )
{
	Destroy( Camera.main.GetComponent<Arkaplan>() );
	Destroy( GetComponent<KusAnimasyon>() );
	yatayHiz = 0;
	engeleCarptim = true;
}

Eğer kuş kameranın dışına çıkmışsa, kuşa sanki engele çarpmış muamelesi yapıyoruz (kod OnTriggerEnter2D‘dekiyle birebir aynı) ve kuşun hareketlerine son veriyoruz.

Böylece çok kısa bir sürede bu bölümü de bitirmiş olduk.

Skor Sistemi

Şimdi sıra geldi kuşun engelleri aştıkça skor yapmasına ve oyun bitiminde yüksekskorun güncellenmesine.

Kuş ne zaman skor yapacak? İki engelin arasını tam yarılamışken skor yapacak. Yani alttaki resimdeki kırmızı çizgiye değdiği an skor yapacak:

resim17

Bir başka deyişle, engelin yarısında yer alan dikey bir çizgiyle temas edince skor yapacağız. Tabi bu çizgi görünmez olacak. Unity’nin 2D motorunda Edge Collider 2D diye bir component var. Nasıl Box Collider 2D bir dikdörtgen şeklindeyse, Edge Collider 2D de bir çizgi şeklinde temas alanı temsil eder.

GameObject-Create Empty yoluyla yeni bir GameObject oluşturun ve hemen ardından objeye Component-Physics 2D-Edge Collider 2D yolunu izleyerek bir Edge Collider 2D verin. Obje resimdeki gibi gözükecektir:

resim18

Burada iki sorunumuz var: 1) çizgi şeklindeki temas alanımız dikey değil, yatay ve 2) bu çizginin uzunluğunu bilmiyoruz. Bu iki sorunu da kod yazarak halledeceğiz. Çizginin ilk noktası kameranın tepe koordinatına denk gelirken ikinci noktası kameranın en alt noktasının koordinatına denk gelecek. Yani çizginin uzunluğu kameranın yüksekliği kadar olacak.

Kod kısmına geçmeden önce, Inspector‘dan Edge Collider 2D component’i altındaki Is Trigger seçeneğini işaretleyin. Böylece kuş çizgiyle temas edince çizginin içinden geçecek. Son olarak, çizgi objemize bir Tag (etiket) verelim. Bunu yapmamızın sebebi şu: kuş objemizdeki OnTriggerEnter2D hem ustEngel ve altEngel için hem de şimdi oluşturduğumuz Edge Collider 2D için çalışacak. Hangisiyle temas ettiğimizi anlamak için temas edilen objenin tag‘ına bakacağız. Objeye Tag vermek için Inspector‘dan Tag-Add Tag… yolunu izleyin:

resim19

Gelen sayfadan Tags sekmesini açın ve Element 0‘a değer olarak SkorTemasAlani verin:

resim20

Şimdi Edge Collider 2D‘li objeyi tekrar seçin ve Inspector‘dan Tag olarak az önce oluşturduğumuz SkorTemasAlani tag’ını verin:

resim21

Artık yapabileceğimiz her şeyi yaptık. Geri kalan kısımları script yoluyla halledeceğiz. Objenin ismini SkorEdgeCollider olarak değiştirin ve objeyi Hierarchy panelinden Project paneline sürükleyerek bir prefab‘a çevirin. Oluşan prefab’ı Prefablar klasörüne atın. Sahnedeki objeyle işimiz bittiğinden sahnedeki SkorEdgeCollider‘ı silin.

Edge Collider objesini oyun sahnemize ekleme işlemini Engeller.cs scriptinde yapacağız. Scripti açıp üst kısımda “public GameObject skorCollider;” adında yeni bir değişken tanımlayın. Sonra Start fonksiyonunun en altındaki for‘u şöyle güncelleyin:

for( int i = 0; i < engelSayisi; i++ )
{
	float xKoordinati = transform.position.x + kameraUnityEbatlar.x + i * ( engelUnityEbatlar.x + ikiEngelArasiMesafe );
	float yKoordinati = Random.Range( -kameraUnityEbatlar.y + altUstEngelArasiBosluk + 0.6f, kameraUnityEbatlar.y - 0.6f );
	ustEngelObjeleri[i] = Instantiate( ustEngel, new Vector3( xKoordinati, yKoordinati, 0 ), Quaternion.identity ).transform;
	altEngelObjeleri[i] = Instantiate( altEngel, new Vector3( xKoordinati, yKoordinati - altUstEngelArasiBosluk, 0 ), Quaternion.identity ).transform;

	EdgeCollider2D temasAlani = Instantiate( skorCollider, new Vector3( xKoordinati + engelUnityEbatlar.x / 2, kameraUnityEbatlar.y, 0 ), Quaternion.identity ).GetComponent<EdgeCollider2D>();
	Vector2[] cizgi = new Vector2[2];
	cizgi[0] = new Vector2( 0, 0 );
	cizgi[1] = new Vector2( 0, -2 * kameraUnityEbatlar.y );
	temasAlani.points = cizgi;
	temasAlani.transform.SetParent( ustEngelObjeleri[i] );
}

Yeni eklediğim kod, “EdgeCollider2D temasAlani = …” satırından başlıyor. Eklediğim kod parçası karmaşık durabilir ama sizi temin ederim çok basit bir mantığı var. Önce sahnede yeni bir SkorEdgeCollider oluşturuyoruz ve bunun Edge Collider 2D component‘ini temasAlani isimli bir değişkende tutuyoruz. Böyle yaparak objenin Edge Collider 2D’sinin değişkenlerine direkt ulaşabiliyoruz. Objeyi oluşturduğumuz konum önemli. X koordinatı olarak en son oluşturduğumuz engelin tam orta noktasını (xKoordinati + engelUnityEbatlar.x / 2) ve Y koordinatı olarak kameranın tepe noktasını (kameraUnityEbatlar.y) veriyoruz.

Sonra cizgi adında 2 elemanlı bir Vector2 array‘i oluşturuyoruz. İki elemanlı çünkü bir düz çizginin sadece iki noktası vardır. cizgi‘nin ilk elemanını 0,0 noktasına konumlandırıyoruz. Bunun anlamı, çizginin başlangıç noktası tam SkorEdgeCollider objemizin bulunduğu noktada olacak. cizgi’nin ikinci elemanın Y koordinatını ise -2 * kameraUnityEbatlar.y olarak belirliyoruz. Bunun anlamı ise, çizginin bitiş noktası, SkorEdgeCollider objesinin Y koordinatından 2 * kameraUnityEbatlar.y kadar aşağıda olacak. SkorEdgeCollider’ın kameranın en üst noktasında yer aldığını düşünürsek, çizginin bitiş noktasının kameranın en alt noktasına denk geldiğini tahmin etmek zor değil. cizgi array‘inin elemanlarına değerlerini verdikten sonra, temasAlani‘nın points isimli değişkenine değer olarak cizgi’yi veriyoruz. Edge Collider 2D component’inin points isminde bir değişkeni bulunmakta ve bu değişken çizgi şeklindeki temas alanının noktalarının koordinatlarını depolamakta. Az önce bu değişkene değer olarak yeni oluşturduğumuz çizgiyi verdik. Son olarak, temasAlani’na parent olarak for‘un içinde en son oluşturulan ustEngel objesini veriyoruz. Böylece temasAlani objemiz o ustEngel objesiyle beraber hareket edecek, ustEngel kameranın dışına çıkıp sağa ışınlanınca temasAlani da sağa ışınlanacak.

Engeller.cs scriptindeki işimiz bitti. Scripti kaydedin, Main Camera‘yı seçin ve Inspector‘dan Skor Collider değişkenine değer olarak SkorEdgeCollider‘ı verin:

resim22

Oyunu başlatın ve hemen ardından pause edin. Scene paneline geçiş yapıp ustEngel objelerinin tekini seçin. Engelin tam ortasından geçen ve sahnenin yüksekliği kadar uzunlukta olan dikey bir temas alanı göreceksiniz. Görevimiz başarılıyla sonuçlandı!

resim23

Şimdi yapmamız gereken şey, kuş bu çizgiyle temas edince ne olacağını ayarlamak. Bu işlemi ise KusHareket.cs scriptinde ayarlayacağız. Script’i açıp yukarıda “private int skor = 0;” şeklinde skor değişkenini tanımlayın. Sonrasında OnTriggerEnter2D fonksiyonunu şu şekilde güncelleyin:

void OnTriggerEnter2D( Collider2D temas )
{
	if( temas.CompareTag( "SkorTemasAlani" ) )
	{
		if( !engeleCarptim && !oyunBitti )
			skor++;
	}
	else
	{
		Destroy( Camera.main.GetComponent<Arkaplan>() );
		Destroy( GetComponent<KusAnimasyon>() );
		yatayHiz = 0;
		engeleCarptim = true;
	}
}

Eğer temas ettiğimiz objenin tag‘ı “SkorTemasAlani” ise (yani SkorEdgeCollider‘a temas ettiysek) ve oyun hâlâ devam ediyorsa (engeleCarptim ve oyunBitti false ise) skoru 1 artırıyoruz. Eğer objenin tag’ı “SkorTemasAlani” değilse, o zaman anlıyoruz ki bir engele çarpmışız ve bu durumda gereğini yerine getiriyoruz.

Oyunu şimdi test ederseniz skorun artıp artmadığını anlayamazsınız çünkü skor private bir değişken ve değerini henüz ekranda bir yere yazdırmıyoruz. Bu sorunu aşmak için skoru ekranda gösterelim. Bizim oyunumuzda skor ekranın sağ üst köşesinde gözükecek. Bunun için GameObject-UI-Text ile sahnede bir yazı objesi oluşturup ismini SkorText yapın ve değişkenlerini şu şekilde güncelleyin:

Şimdi bu yazıyı skoru gösterecek şekilde güncelleyelim. Bunun için KusHareket.cs script’inin başında “public UnityEngine.UI.Text skorText;” değişkeni oluşturun. Inspector’dan bu değişkene değer olarak Hierarchy’deki SkorText objesini verin. Ardından KusHareket.cs script’inin Update fonksiyonunun sonuna “skorText.text = “SKOR: ” + skor;” satırını ekleyin. Bu şekilde skorText’in yazısını, hep skorun değerini gösterecek şekilde değiştiriyoruz. Şimdi oyunu test ederseniz, sağ üstte skoru görebilirsiniz.

Sıra geldi yüksekskora. Yüksekskor, round bitiminde gerekli olursa güncellenecek ve cihaza kaydedilecek. Böylece oyunu sonradan açınca yine aynı yüksekskorla karşılaşacağız. Yüksekskor için KusHareket.cs scriptinde “private int yuksekSkor = 0;” değişkeni tanımlayın. Sonra OnCollisionEnter2D fonksiyonunu şöyle güncelleyin:

void OnCollisionEnter2D( Collision2D temas )
{
	Destroy( Camera.main.GetComponent<Arkaplan>() );
	Destroy( GetComponent<KusAnimasyon>() );
	yatayHiz = 0;
	oyunBitti = true;

	if( skor > yuksekSkor )
	{
		yuksekSkor = skor;

		PlayerPrefs.SetInt( "YuksekSkor", yuksekSkor );
		PlayerPrefs.Save();
	}
}

Hatırlayacağınız üzere, OnCollisionEnter2D fonksiyonu kuş toprak‘a çarpınca, yani round bitince çalışıyordu. Yaptığımız şey ise şu: round bitince eğer skor yüksekskordan büyükse, yüksekskoru skora eşitliyoruz ve cihaza “YuksekSkor” ismiyle kaydediyoruz. PlayerPrefs.SetInt, cihaza bir tamsayı değeri kaydetmeye yarar. İlk parametresine kaydedilen değer için bir isim girilir ve ikinci parametresine de kaydedilecek değer girilir. Bu komutu kullandıktan sonra, PlayerPrefs.Save komutu ile değerin cihaza tam o anda kaydedilmesini sağlıyoruz. Aksi taktirde yuksekSkor‘un değeri cihaza oyundan çıkarken kaydedilir, oyun olur da çökerse kayıt işlemi suya düşer.

Geriye oyunun başında yüksekskoru cihazdan çekmek kaldı. Bunun için de KusHareket.cs‘nin Start fonksiyonunu şu şekilde güncelleyin:

private void Start()
{
	Camera kamera = Camera.main;
	kameraUnityEbatlar = new Vector2( kamera.orthographicSize * kamera.aspect, kamera.orthographicSize );
	yuksekSkor = PlayerPrefs.GetInt( "YuksekSkor" );
}

PlayerPrefs.GetInt komutu, cihazdaki kayıtlı bir tamsayı değerini çekmeye yarar. Parametre olarak da, kaydedilen değere verilen ad girilir. Eğer cihazda böyle bir değer kaydedilmemişse 0 döndürülür.

Şimdi haklı olarak yüksekskoru da ekranda görmek isteyeceksiniz. Bunun için Update fonksiyonundaki skoru ekrana yazdıran satırı şu şekilde güncelleyin: “skorText.text = “SKOR: ” + skor + “\nYÜKSEKSKOR: ” + yuksekSkor;

“\n” komutu, yazıda bir satır aşağı inmeye yarar. Yaptığımız şey skorun bir satır altına yüksekskorun değerini yazdırmak. Oyunu iyice test edin, yüksekskordan büyük bir skor elde edip ölün, yeniden başlayın. Oyunu kapatıp tekrar açın. Yüksekskorun her yeni oyunda değerini koruması lazım.

Ses Efektleri Eklemek

Oyun boyunca çalacağımız 4 ses var:

kanatSes: ekrana tıklayınca çalan kanat çırpma sesi

skorSes: skor yapınca çalan ses

olumSes: bir engele veya toprağa çarpınca çıkan ses

dusmeSes: bir engele çarpınca çıkan düşme sesi

Ses çaldırma işlemlerinin hepsini KusHareket.cs scriptinde halledeceğiz. Ama önce FlappyBird objemize Component-Audio-Audio Source yoluyla Audio Source component’i vermemiz lazım. Böylece FlappyBird objesindeki scriptler ses dosyası çalma yetkisine sahip olacaklar.

Şimdi de kullanacağımız ses dosyalarını Unity’e import edelim. Önce Project panelinde “Sesler” adında yeni bir klasör oluşturun ve Assets-Import New Asset… yoluyla dersin başında paylaştığım Winrar arşivindeki tüm dört ses dosyasını da tek tek Unity’e import edin:

resim24

İki çeşit ses bulunmaktadır: 3D ve 2D. 3D seslerin şiddeti, onları çaldıran AudioSource component’inin bulunduğu konuma göre artar ya da azalır ama 2D seslerin şiddeti her zaman aynıdır. Biz 2D oyunumuzda seslerin 2D çalmasını istiyoruz, bu yüzden FlappyBird’deki AudioSource component’inin Spatial Blend değerinin 2D olduğundan emin olun.

Sıra geldi script vasıtasıyla uygun sesleri uygun zamanda çaldırmaya. Bunun için KusHareket.cs scriptinin en başında şu dört değişkeni tanımlayın:

public AudioClip kanatSes;
public AudioClip skorSes;
public AudioClip olumSes;
public AudioClip dusmeSes;

Sonra OnTriggerEnter2D ve OnCollisionEnter2D fonksiyonlarını şöyle değiştirin:

void OnTriggerEnter2D( Collider2D temas )
{
	if( temas.CompareTag( "SkorTemasAlani" ) )
	{
		if( !engeleCarptim && !oyunBitti )
		{
			skor++;
			GetComponent<AudioSource>().PlayOneShot( skorSes );
		}
	}
	else
	{
		Destroy( Camera.main.GetComponent<Arkaplan>() );
		Destroy( GetComponent<KusAnimasyon>() );
		yatayHiz = 0;

		if( !engeleCarptim )
		{
			GetComponent<AudioSource>().PlayOneShot( olumSes );
			Invoke( "DusmeSesiCal", 0.25f );
			engeleCarptim = true;
		}
	}
}

void OnCollisionEnter2D( Collision2D temas )
{
	Destroy( Camera.main.GetComponent<Arkaplan>() );
	Destroy( GetComponent<KusAnimasyon>() );
	yatayHiz = 0;
	oyunBitti = true;

	if( skor > yuksekSkor )
	{
		yuksekSkor = skor;

		PlayerPrefs.SetInt( "YuksekSkor", yuksekSkor );
		PlayerPrefs.Save();
	}

	if( !engeleCarptim )
		GetComponent<AudioSource>().PlayOneShot( olumSes );
}

void DusmeSesiCal()
{
	GetComponent<AudioSource>().PlayOneShot( dusmeSes );
}

GetComponent<AudioSource>().PlayOneShot komutu, bir ses dosyası çalmaya yarar. Bu komutu kullanarak, skor yapınca skorSes‘i çaldırıyoruz. Bir engele çarpınca eğer engeleCarptim false ise olumSes‘i çalıyor, ardından Invoke fonksiyonu vasıtasıyla 0.25 saniye sonra dusmeSes‘i çalıyoruz. İkisini de aynı anda çalarsak çıkan ses çok güzel durmadığından böyle bir yöntem kullanıyoruz. Invoke, bir fonksiyonun belirli bir saniye sonra otomatik olarak çağrılmasını sağlar. if( !engeleCarptim ) kullanmamızın sebebi, kuşun ölme sesinin sadece bir kere çalınacağından emin olmak. Kuş direkt toprak‘a çarparsa da sadece olumSes‘i çaldırıyoruz.

Şimdi kanat çırpma sesi verelim. Bunun için Update‘teki “if( Input.GetMouseButtonDown( 0 ) && !engeleCarptim )” kodunu şöyle güncelleyin:

if( Input.GetMouseButtonDown( 0 ) && !engeleCarptim )
{
	dikeyHiz = ziplamaGucu;
	GetComponent<AudioSource>().PlayOneShot( kanatSes );
}

Ses çaldırmadığımız tek bir yer kaldı: kuş ekranın dışına çıkarsa öldüğü zaman. Bunun için de Update() fonksiyonunda “if( transform.position.y > kameraUnityEbatlar.y )” koşulunun içini şöyle güncelleyin:

Destroy( Camera.main.GetComponent<Arkaplan>() );
Destroy( GetComponent<KusAnimasyon>() );
yatayHiz = 0;

if( !engeleCarptim )
{
	GetComponent<AudioSource>().PlayOneShot( olumSes );
	Invoke( "DusmeSesiCal", 0.25f );
	engeleCarptim = true;
}

Scripti kaydedin ve FlappyBird‘ün Inspector paneline gelin. Buradan Kus Hareket (Script) component’indeki yeni eklediğimiz değişkenlere, uygun ses dosyalarını Project panelinden sürükleyerek değer olarak verin:

Şimdi oyunu çalıştırın. Uygun zamanda uygun ses dosyaları çalınacaktır.

Son Rötuşlar

Flappy Bird’ü az çok oynamışsanız fark etmişsinizdir; bizim oyunumuzda kuşun hızı olsun zıplama yüksekliği olsun orijinal Flapp Bird’den biraz daha farklı. Bunu oyunun yarılarındayken ben de fark ettim ama düzeltmek için dersin sonuna gelmeyi bekledim. Deneye yanıla, aşağıdaki değişkenlere Inspector‘dan yanlarındaki değeri verince oyunun orijinaline daha da benzediğini buldum. İsterseniz siz de değerleri benimki gibi değiştirebilirsiniz ya da tamamen kendi zevkinize göre kişiselleştirebilirsiniz.

Kus Hareket (Script)

Yercekimi: 8

Ziplama Gucu: 4

Yatay Hiz: 1

Arkaplan (Script)

Gokyuzu Saga Kayma Hizi: 0.8

Engeller (Script)

Alt Ust Engel Arasi Bosluk: 1.45

Iki Engel Arasi Mesafe: 1.2

Buna ek olarak, ESC tuşuyla oyundan çıkma özelliği de eklesek iyi olacak. Bu özellik Unity Editor’de çalışmayacak ama oyunu Build edince çalışacak. Tek yapmanız gereken, KusHareket.cs scriptinin Update fonksiyonunun en altına şu kodu yazmak:

if( Input.GetKeyDown( KeyCode.Escape ) )
	Application.Quit();

Oyunu Android’e Build Etmek

Çoğunuz oyunu Android cihazınızda da test etmek istiyorsunuzdur. Bunu anlayışla karşılıyorum. Size iyi bir haberim var: Android için tek bir satır yeni kod yazmamıza gerek yok. Çünkü Input.GetMouseButtonDown(0) komutu Android’de ekrana parmakla dokununca da true dönüyor. Ve Input.GetKeyDown(KeyCode.Escape) komutu da Android cihazda geri tuşuna basınca true dönüyor.

File-Build Settings yolunu izleyin. Gelen pencerede Platform olarak Android‘i seçin ve Switch Platform deyin. Sonra oradaki Player Settings… butonuna tıklayın.

resim27

Inspector‘da açılan sayfada Other Settings altındaki Bundle Identifier‘ı “com.Unity.FlappyBird” olarak değiştirin:

resim28

Şimdi Build Settings‘teki Build butonuna basıp oyunun APK şeklindeki son versiyonunu bir yere kaydedin (Bu işlemden önce Android SDK kurmanız lazım. Kurmadıysanız şu derse göz atın: https://yasirkula.com/2013/07/17/unity-android-sdk-kurulumu-resimli-anlatim/ ).

Build işlemi bitince APK dosyasını Android cihazınıza atıp kurun ve oyunu test edin. Elinize sağlık!

The End

Bu uzunca dersin böylece sonuna gelmiş bulunmaktayız. Daha başka derslerde görüşmek dileğiyle, hoşçakalın 🙂

yorum
  1. oyunkurdu dedi ki:

    Hocam merhaba, belirli bir skor sonrası oyun hızını nasıl artırabilirim?

    • yasirkula dedi ki:

      Yorum geldiğine dair bildirim almadım o yüzden yorumunuzu geç gördüm afedersiniz. “skor++;” yaptığınız satırdan sonra, skor’un belli bir değere eşit olup olmadığına bakabilir ve eğer eşitse, yatayHiz değerini istediğiniz gibi artırabilirsiniz.

  2. seyit dedi ki:

    merhaba
    hocam tekrar tekrar kontrol etmeme rağmen oyunun engelleri az sıklıkla dengesiz geliyor. şöyleki üst engel ekranı ortalamış alt engel çıkmamış gibi veya üst engelin üstünde boşluk alt engelin ucu çok az gözüküyor gibi. Birde telefon ekran ayarını tam oturtturamadım sanırım android cihazlar için ekran ayarı nasıl olmalı.teşekkürker

    • yasirkula dedi ki:

      Yaşadığınız sıkıntının aslında olmaması lazımdı, projenin bitmiş halini indirip denediniz mi? Onda da aynı şey oluyor mu? Özel bir ekran ayarı yapmanıza gerek olmaması lazım.

      • seyit dedi ki:

        hocam ekran ayarını 1920×1080 portrait yaparak çözdüm. ancak engellerle ilgili tutarsızlık hala devam ediyor. bitmiş projeyi indirdim öyle bir durum yok. engelleri silip tekrardan tanımladım ancak hala aynı değişen bir durum olmadı. sanırım bir yeri atladım ama nereyi tam olarak çözemedim 🙂

      • yasirkula dedi ki:

        Aklıma ilk gelen şey, engellerin pivot ayarları. Yaptığınız projedeki ve projenin bitmiş halindeki engel sprite’larını karşılaştırıp, bir fark var mı bakmanızı öneririm. Akabinde engel prefab’larını da karşılaştırabilirsiniz.

  3. enes alaşkan dedi ki:

    hocam ben daha yeni başlıyorum bu oyunu uyarlayıp playstoreda paylaşıcam şimdi biri çarpıp öldüğünde onun önüne 2 seçenek vermek istiyorum reklam izle devam et yada öl diye bunu nasıl yapablirim daha projeye başlamadım aklımdayken sorayım dedim cahilee anlatıyo gibi anlatırsanız sevinirim 🙂

  4. AliHD dedi ki:

    Hocam merhaba;
    Uygun konubaşlığı bulamadığım için sorumu buraya yazıyorum kusura bakmayın. Sorun şu şekildebir a değişkenine random 1 ila 100 arasında değer atıyoruz. Sonra da her a değeri için oyunda farklı bir işlem yaptırmak istediğimizden ıf komutları yazıyoruz 100 tane alt alta ıf(a==1) ise bu olsun ıf(a==2) ise bu olsun gibi alt alta koşul kodu yazmak ile önce ıf(a<=50) ise yazıp sonra bu ıf koşulunun içinde ıf(a25) olarak iki dala ayırıp yine bu koşulları dallandırıp bölerek yazmak arasında oyunun hızlı çalışması açısından bir fark var mıdır? yani ikinci dediğimde mesela a 6 olsun ilk dal ayrımınında 50’den yukarıları boşuna taramıyor diye düşünebilir miyiz veya başka bir çözüm öneriniz var mıdır ? bu tarz oyunlarda

    • yasirkula dedi ki:

      (a<=50) yöntemi aslında binary search algoritması ile aynı. Bence de bu yöntem, 1’den 100’e kadar her şeyi == ile kıyaslamaya göre daha mantıklı çünkü doğru if’i bulmak ortalama daha az deneme-yanılma tutacak. Ancak bu 1’den 100’e kadar işlemin hepsi birbirinin neredeyse birebir aynısıysa (mesela materyalin texture’unu a==0 iken texture0, a==1 iken texture1 yapıyorsanız), bir array ile hiç bir if kullanmadan sorunu çözebilir misiniz kontrol edin (mesela Texture[] array’i oluşturup materyalin texture’unu arrayObjesi[a]’ya eşitlersiniz, 100 texture’u da Inspector’dan array’e değer olarak verirsiniz).

  5. Arda dedi ki:

    Sonsuz arkaplan kodunu c#’e uyarlayıp yazdım ama kod çalışmadı.

  6. Ali dedi ki:

    Hocam merhabalar,

    Size bir sorum olacaktı şimdi bir oyun yüklediğimiz zaman öce oklar ile vb. objeler ile nasıl oynanacağını tarif ediyor ondan sonra o kısım bir daha oyun telefonda yüklü iken çıkmıyor onun kodunu nasıl oluşturuyorlar. Yani diyelim ki bir canvas ile oyunu anlatan bir kısım yaptık ama sadece oyun telefona yüklendiğinde çıkmasını istiyoruz oyunu aç kapama kısmında çıkmaması lazım start kısmını versek her oyun oynandığında çıkıyor nasıl yapabiliriz. teşekkürler

    • yasirkula dedi ki:

      Eğer PlayerPrefs.GetInt(“Tutorial”)’ın değeri 0 ise tutorial canvas’ını aktif hale getirebilir, oyuncu canvas’ı kapattıktan hemen sonra ise PlayerPrefs.SetInt(“Tutorial”,1) ile artık canvas’ın gözükmemesini sağlayabilirsiniz.

  7. Ali dedi ki:

    Hocam Merhabalar ,

    Bazı oyunlarda oyunun arkaplan sesini kaldırmak ama diğer ara sesleri duymak için bir buton oluyor . Ben butonu yapıyorum fakat sahneyi tekrar başlatınca yine arkaplan müziği çıkıyor. Bir playerprefs.setstrin gibi bir kayıtla sesin açılımı kapalımı oldugunu kaydetmeyi düşünüyorum fakat kodlama kısmı bir türlü oturtamadım yardımcı olabilirmisiniz nasıl bir kod yazmalı

    • yasirkula dedi ki:

      PlayerPrefs.SetInt(“MuzikAcik”,1) ile müziği açıp PlayerPrefs.SetInt(“MuzikAcik”,0) ile müziği kapatın. Müziğin açık olup olmadığına bakmak için de bool muzikAcikMi = PlayerPrefs.GetInt("MuzikAcik") == 1; kodunu kullanın.

Cevap Yazın

Aşağıya bilgilerinizi girin veya oturum açmak için bir simgeye tıklayın:

WordPress.com Logosu

WordPress.com hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Facebook fotoğrafı

Facebook hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.