Unity NavMesh Kullanarak Basit Bir Futbol Oyunu Yapmak

Yayınlandı: 24 Ocak 2016 yasirkula tarafından Oyun Tasarımı, UNITY 3D içinde

Hepinize merhabalar,

Bu dersimizde birlikte Unity 3D‘nin NavMesh sistemini kullanarak basit bir oyun yapmaya çalışacağız. NavMesh, yani Navigation sistemi Unity’nin bize pathfinding (yani bir objenin bir noktadan başka bir noktaya, yol üzerindeki engelleri göz önüne alarak bir rota çizmesi) için sunduğu bir araç.

Yapacağımız basit oyunda amacımız toplam 60 saniye boyunca topu ayağımızda tutabilmek. Tabi rakibimiz (AI) bizden önce davranırsa oyunu kaybediyoruz. Her ne kadar NavMesh sisteminden bahsedecek olsak da dersin büyük çoğunluğunda oyun mekaniklerini oluşturmakla uğraşacağız. NavMesh’e özellikle dersin başlarında yoğunlaşacak, sonra yeri gelince öğrendiğimiz bilgileri pratiğe vuracağız. Scriptlerimizi ise C# dilinde yazacağız. Eğer Unity konusunda hiç bilginiz yoksa bu derse başlamadan önce başlangıç dersleri okumak/izlemek isteyebilirsiniz. Özellikle kod yazma konusunda biraz da olsa tecrübeniz olması çok iyi olur.

Bu derste ilk defa animasyonlu gif resimler kullandım. Bu resimleri normal resimlerden ayırt etmek için köşelerine “gif” yazısı yerleştirdim. Ayrıca şunu da şimdiden belirteyim: bu ders uzun, çok uzun. Word’de yaklaşık 70 sayfa tuttu (bir kısmı resimler ve kodlardan oluşuyor ama yine de oldukça uzun bir yazı bekleyin). Derste oyunu her şeyiyle sıfırdan oluşturuyoruz. Buyurun size oyundan bir resim:

0

Oyunu Web Player üzerinden test etmek için tıklayın: http://yasirkula.freeiz.com/Projects/MiniFootball.html

Oyunu WebGL üzerinden test etmek için tıklayın: http://yasirkula.freeiz.com/Projects/MiniFootballWebGL/index.html

Örnek projeyi indirmek için tıklayın (Unity 5.3.1 öncesi sürümlerde sıkıntı çıkabilir): https://www.dropbox.com/s/g6y8dm0g99i1c9q/NavMesh%20Ders.zip?dl=0

Dersi PDF olarak indirmek isterseniz tıklayın (dikkat: gif resimler desteklenmez ve derste hatalı bir kısım olursa ben o hatayı site üzerinden güncellerken büyük ihtimalle pdf üzerinden güncellemem [hatalı kısım olduğunu sanmıyorum gerçi]): https://www.dropbox.com/s/9q7d5f4m9je1hin/Unity%20NavMesh%20Futbol%20Oyunu%20Ders.pdf?dl=0

O halde son gaz derse başlayalım…

Yeni Bir Proje ve Işıklandırma

Yeni bir 3D proje açın; projeye istediğiniz ismi verin. Öncelikle ışıklandırma ile ilgili biraz ayar yapmak istiyorum. Ne skybox‘ı (gökyüzü) ekrana çizdirmek istiyorum ne de skybox’ın sahneyi ışıklandırmasını. Oyunda arkaplanımız beyazımsı basit bir renkten oluşacak sadece.

Main Camera objesini seçip Inspector‘dan “Clear Flags” seçeneğini “Solid Color” yapın. Artık skybox ekrana çizdirilmeyecek, onun yerine arkaplan tek bir renkten oluşacak. Background (arkaplan) rengini ben açık gri yapmayı tercih ettim (bu değişikliği Scene panelinde değil ama Game panelinde görebilirsiniz):

1

Skybox’ı çizdirmiyoruz ancak skybox hâlâ ışıklandırmaya etki ediyor. Bunu görmek için GameObject-3D Object-Cube yolunu izleyerek sahnede bir küp oluşturup ona odaklanın (obje seçili ve mouse Scene paneli üzerinde iken F tuşuna basın). Sonrasında Window-Lighting yolunu izleyerek Lighting panelini açın ve oradaki Skybox‘ın değerini None olarak, “Ambient Source“un değerini ise Color olarak değiştirin. Artık skybox ışıklandırmaya etki etmiyor, onun yerine “Ambient Color” isimli tek bir renk etki ediyor. Şu anda Ambient Color koyu olduğu için küp de biraz soluk. Bunun için Directional Light’ın gücünü artırabilir, “Ambient Intensity” ile Ambient Color’ın gücünü artırabilir ya da direkt Ambient Color’ı daha açık bir renkle değiştirebiliriz. Ben üçüncü yolu tercih ederek Ambient Color’ı daha açık (ve biraz mavimsi) bir renkle (rgb[166,163,180]) değiştirdim (bu değerleri kafanıza göre belirleyebilirsiniz, tamamen kişisel bir tercih):

2

EKLEME: Ambient Intensity‘i sonradan 0.5 yaptım çünkü öbür türlü küp ekranda çok parlak duruyordu.

Sonuç olarak küp objemize etki eden ışık biraz daha basit (ve bence şirin) hale geldi:

3

Son olarak da Lighting panelinin en altındaki “Auto” seçeneğini kaldırın. Bu seçenek lightmap oluşturmakla alakalı; bizse lightmap oluşturmak istemiyoruz:

3_1

Işıklandırmayla ilgili yaptığımız bu basit değişiklikleri kaybetmemek için hemen şimdi sahnemizi (scene) kaydedelim (CTRL+S). Sahneye “Oyun” ismini verin. Artık NavMesh’i ufaktan tanımaya başlayabiliriz!

Navigation Sistemine Giriş

Sahnedeki küp objesinin Transform‘unun Scale değerini (10, 1, 10) yaparak onu XZ ekseninde 10 kat genişletin. Ardından GameObject-3D Object-Cube ile sahnede yeni küp objeleri daha oluşturup onları şuna benzer şekilde dizin (objeleri daha rahat görebilmek için sahneye geçici olarak bir Spotlight ekledim):

4

NavMesh sisteminin çalışma mantığı şöyle ki; sahnedeki sabit (static) olan engeller göz önüne alınarak yapay zekanın üzerinde yürüyebileceği alan hesaplanıyor ve bir yerden bir yere giderken çizilen rota bu alan üzerinden belirleniyor. Kısacası yoldaki sabit (yeri kesinlikle değişmeyecek olan) engeller birer static obje olmalı (zemin de dahil). Bunun için zemin de dahil olmak üzere tüm küp objelerini seçin ve Inspector‘un tepesindeki Static kutucuğunu işaretleyin:

5

Objeleri static yaptıktan sonra Window-Navigation yolunu izleyerek Navigation panelini açın:

6

Bu panelde, yapay zekaya sahip olacak olan (NavMesh’ten faydalanacak) karakterin özelliklerini kabaca belirliyoruz. Diyebilirsiniz ki benim her karakterim aynı sıfatlara sahip olmak zorunda değil. Eğer ebatları çok farklı olan çeşitli düşmanlarınız varsa o zaman işiniz zor zira NavMesh sadece tek bir düşmanın özelliklerini girmenize olanak sağlıyor.

Navigation panelindeki “Agent Radius“, yapay zekanın kabaca yarıçapını belirlerken “Agent Height“, yapay zekanın yüksekliğini belirliyor (yapay zekanın kafasını çarpabileceği yükseklikte engellerin olduğu yerler NavMesh’e dahil olmuyor). “Max Slope“, üzerinde gidilebilecek eğimli arazinin maksimum eğimini, “Step Height” ise üzerine çıkılabilecek basamakların (örneğin merdiven basamağı veya yerdeki ufak bir engel) maksimum yüksekliğini belirliyor. “Off Mesh Links” ile alakalı kısımlar ise bu derse dahil değil (ne yalan söyleyeyim ben de bilmiyorum henüz bu off mesh link denen olayı).

Yapay zekanın sıfatlarını belirledikten sonra NavMesh’in yürünebilir alanı hesaplamasını sağlamak için Navigation panelinin altındaki Bake butonunu kullanıyoruz. Ben normal değerleri ellemeden Bake yaptığımda şöyle bir sonuç elde ettim (Scene panelinde):

7

Gördüğünüz bu mavi alan, yapay zekanın üzerinde yürüyebileceği alanı temsil ediyor. Bu alan ile küplerin kenarları arasında biraz mesafe olduğunu fark etmişsinizdir. Çünkü bu mavi alan, yapay zekanın bir engele temas etmeden hareket edebileceği alanı gösteriyor; yani yapay zekanın pozisyonu bu mavi alan üzerinde olursa o zaman bir engele temas etmeyeceği bize garanti ediliyor. Bu sebepten ötürü de hesaplama yapılırken engellere yarıçap kadar yakın mesafeler kırpılıyor (ve mavi yüzey, kenarlardan yarıçap kadar uzakta oluyor). Örneğin ortadaki iki küp arasında incecik bir mavi yol var. Başta düşünebilirsiniz ki bu yola ancak minicik bir yapay zeka sığabilir. Ancak aslında bu alan “Agent Radius“a sahip yapay zekanın yürüyebileceği alanı temsil ettiği için “Agent Radius”a sahip herhangi bir karakter o iki küpün arasından rahatlıkla geçebilir (ancak çok manevra kabiliyeti olmaz çünkü mavi yol orada oldukça daralıyor).

Eğer yarıçapı yarıya indirip maksimum eğimi biraz düşürürsem (ilk yokuştan yüksek ancak ikinci yokuştan düşük bir eğime) şöyle bir sonuç elde ediyorum:

8

Gördüğünüz üzere mavi alan ile kenarlar arasındaki uzaklık, yarıçap azaldığı için azaldı. İkinci yokuşun eğimi fazla olduğu için oradaki mavi alan kalktı ve bazı küplerin tepesinde mavi alanlar oluştu. Bu da demek oluyor ki eğer o küplerden birinin üzerine bu yarıçapta bir yapay zeka koyarsak bu yapay zekanın o küpün üzerinde bir uçtan diğer uca yürümesi mümkün. Ancak küpün tepesi ile zemin arasında herhangi bir bağlantı olmadığı için yapay zekanın bu iki alan arasında geçiş yapması mümkün değil (benzer bir şekilde, ikinci yokuşun tepesindeki mavi alan da soyutlanmış durumda).

Dilerseniz hazır basit bir arena oluşturmuşken bu arenada hareket eden çok basit bir de yapay zeka oluşturalım. Oyun alanında nereye tıklarsak yapay zeka oraya gitsin (ya da gitmeye çalışsın). GameObject-3D Object-Capsule ile sahnede bir kapsül oluşturup onu arenada istediğiniz yere sürükleyin. Objenin ebatlarının Navigation panelinde girdiğiniz değerlerden büyük olmamasına özen gösterin. Kapsül objesi seçili iken Component-Navigation-Nav Mesh Agent yolunu izleyin.

Nav Mesh Agent component’i, NavMesh’te yapay zekayı hareket ettirmeye yarayan önemli bir component. Bir objede hem Nav Mesh Agent hem de Rigidbody varsa Rigidbody’deki “Is Kinematic“i işaretleyerek iki component’i birbirini engellemesini önlemeniz gerekir (neyse ki şu anda öyle bir durumumuz yok).

9

Bu component, gördüğünüz üzere, sadece birkaç basit değişken kullanıyor. En başta yapay zekanın yarıçapını, toplam yüksekliğini ve yerden yüksekliğini (offset) belirliyoruz. Buradaki yerden yükseklikten kasıt, objenin pivot noktasının yerden ne kadar yüksekte olduğu. NavMesh sistemi yapay zekayı konumlandırırken onu yerden offset kadar yükseğe koyuyor. Navigation sekmesinde zaten bir radius (yarıçap) girdiğimiz için niçin burada da giriyoruz diyebilirsiniz. Eğer ki burada girdiğimiz yarıçap Navigation’da girdiğimizden küçükse tahmin edebileceğiniz üzere bu obje de NavMesh’ten faydalanabilir. Onun da dışında bir sebep var; o da Obstacle Avoidance olayı. NavMesh kullanan objeler (yapay zeka veya Agent da diyebiliriz) birbirleri ile mümkün olduğunca temas etmeden hareket etmeye çalışıyorlar ve bu yüzden birbirlerinin sıfatları hakkında bilgi sahibi olmaları gerekiyor.

Steering başlığı altında sırayla objenin maksimum hızını, dönme hızını (açısal hız) ve hızlanma ivmesini görüyoruz. “Stopping Distance” değeri, yapay zekanın hedef noktadan ne kadar uzakta duracağını belirlemeye yarıyor. Varsayılan olarak hedefin tam üstünde durmak istediğimiz için bu değer 0 olarak belirlenmiş. “Auto Braking” ise yapay zekanın hedefe yaklaşırken yumuşak bir şekilde yavaşlamasını sağlıyor. Eğer bu seçeneği kapatırsak obje hedef noktanın üzerinde bir ileri bir geri zikzak çizmeye başlar. Niye bu seçeneği kapatmak isteyeyim diye soracak olursanız da cevabı; devriye gezen bir yapay zekanın bu devriyeyi hızından ödün vermeden yapması diyebilirim.

Obstacle Avoidance sekmesi altındaki Quality değişkeni, yapay zekaların birbirlerinden kaçınırken kullandıkları algoritmanın kalitesini belirliyor. Daha kaliteli bir algoritma maalesef ki daha yavaş çalışan bir algoritma anlamına geliyor. Priority ise yapay zekanın sahip olduğu önceliği belirliyor. Priority’si sayısal anlamda düşük olan yapay zekalar daha yüksek önceliğe sahip oluyor. Önceliği az olan objeler önceliği yüksek olan objelere mümkün olduğunca yer vermeye çalışıyor, böylece yüksek öncelikli objeler rotalarına minimum engelle karşılaşarak devam ediyorlar.

Path Finding başlığı altındaki “Auto Repath” seçeneğini şöyle açıklayayım: eğer yapay zekanın hedef noktaya gitmesi mümkün değilse (yolda engel olabilir ya da geçişi engelleyen başka birşey) o zaman yapay zeka, hedefe en yakın “gidilebilir” noktaya doğru hareket ediyor. Eğer Auto Repath seçeneği aktifse, bu en yakın gidilebilir noktaya varınca asıl hedef noktaya tekrar bir güzergah çizilmeye çalışılıyor (belki en yakın noktaya gidene kadar yoldaki engel kalkmıştır umudu ile). “Area Mask” seçeneği ise bu Agent’ın üzerinde yürüyebileceği NavMesh layer‘larını belirliyor. Bu layer sistemi de aslında oldukça ilginç bir şey. Navigation panelinden Areas sekmesine geçiş yaparsanız karşınıza NavMesh layer’ları geliyor:

10

Buradaki her layer’ın bir zorluk derecesi (cost) var. Diyelim ki yapay zekanın hedefe ulaşmak için gidebileceği eşit uzunlukta 2 rotası varsa, bu iki rotadan Cost değeri küçük olan layer’a ait olanı tercih ediliyor (ikisi de aynı layer’daysa herhalde rastgele seçiliyordur). Peki NavMesh’imize nasıl layer’lar atayabiliyoruz? Öncelikle “Yokus” isminde ve Cost’u 3 olan yeni bir layer oluşturalım:

11

Ardından Navigation panelinin Object sekmesine geçiş yapalım. Sonrasında ise sahnedeki yokuş objelerini Scene veya Hierarchy panelinden seçelim. Karşımıza şöyle bir arayüz gelecek:

12

Buradan “Navigation Area“ya gördüğünüz gibi Yokus layer’ını veriyoruz. Şimdi NavMesh’i tekrar Bake edecek olursak da:

13

Gördüğünüz üzere layer’ları, renkleri vasıtasıyla rahatça birbirlerinden ayırt edebiliyoruz. Bu layer değiştirme olayı bu örnekte birşeyi değiştirecek mi? Hayır, çünkü tepeye çıkmaya yarayacak alternatif bir rota yok. Bu örnek sadece layer’ları gösterme amaçlıydı.

Bence artık oluşturduğumuz kapsül objesinin dokunduğumuz yere gitmesinin vakti geldi. “DokundugumYereGit” adında yeni bir C# script oluşturun:

using UnityEngine;

public class DokundugumYereGit : MonoBehaviour 
{
	private NavMeshAgent yapayZeka;
	
	// Start fonksiyonu oyun başladığı vakit tek seferlik çalıştırılır
	void Start()
	{
		// Nav Mesh Agent component'ini bir değişkene at
		yapayZeka = GetComponent<NavMeshAgent>();
	}
	
	// Update fonksiyonu oyun boyunca sürekli çağrılır (her frame'de)
	void Update ()
	{
		// eğer ekranda herhangi bir yere
		// sağ mouse tuşuyla tıklarsak
		if( Input.GetMouseButtonDown(1) )
		{
			// fare imlecinin olduğu noktadan 3d uzaya bir ışın (ray) yolla
			RaycastHit hit;
			Ray tiklamaRay = Camera.main.ScreenPointToRay( Input.mousePosition );
			
			// eğer yolladığımız ışın bir obje ile temas ederse
			if( Physics.Raycast( tiklamaRay, out hit ) )
			{
				// tıklanılan noktayı (hit.point) hedef olarak belirle
				yapayZeka.destination = hit.point;
			}
		}
	}
}

Bu scriptin yaptığı şey çok basit: oyun ekranında herhangi bir yere sağ tıklarsak (GetMouseButtonDown(1)) tıklanılan noktaya bir ışın (Ray) yollanıyor ve bu ışın bir objeyle temas ederse, yapay zekaya ışının dokunduğu noktaya hareket etmesi söyleniyor. Eğer ekrana sol tıklayınca hareket etmesini istersek “Input.GetMouseButtonDown(0)” yazabiliriz.

Scripti kapsül objesine component olarak verin, Main Camera‘yı sahneyi güzelce görebilecek bir açıya taşıyın ve oyunu çalıştırın. Game panelinde bir yere sağ tıkladığınız vakit eğer tıkladığınız yer gidilebilecek bir yerse kapsül direkt oraya, yoksa oraya en yakın olan noktaya hareket edecek. Kapsül hareket halindeyken Scene paneline geçiş yaparsanız (Navigation paneli de açık vaziyette iken) kapsülün çizdiği rotayı kabaca görebilirsiniz. Gidilecek rota üzerindeki alan biraz daha koyu renkte gözüküyor (aşağıdaki resimde kapsülü haritanın sağ ucundan haritanın tepe noktasına hareket ettiriyorum):

14

NavMesh ve Dinamik Engeller

Şu ana dek hep sabit olan (static) öğeler ile çalıştık ancak bazen hareket eden, yani dinamik objeler de yapay zekanın hareket edebileceği alana etki eder. Neyse ki NavMesh’in dinamik engellere de desteği bulunmakta. Bu engellerin Inspector‘unda static kutucuğu işaretli olmaz ve özel bir component vasıtasıyla NavMesh ile birlikte çalışırlar. Bu component ile çalışmadan önce arenayı sağ taraftan şu şekilde genişletelim:

15

Burada yeşil renk ile gösterdiğim dikdörtgenler prizması kendi etrafında 90 derece dönebilecek ve her 90 derece dönüşünde orada alternatif bir yol oluşacak/yok olacak. Eğer obje static ise onun static’liğini elinden alın. Sahnenin bu halini NavMesh’e tanıtmak için Navigation ekranından tekrar bir Bake yapın. Sonrasında ise Component-Navigation-Nav Mesh Obstacle yolu ile yeşil objeye ilgili component’i verin:

16

Şu anda iki şekil dinamik engel destekleniyor: Box (dikdörtgenler prizması şekilli) ve Capsule (örneğin yerde yuvarlanan bir varil için kullanılabilir). Center ve Size değişkenleri ile tıpkı Collider component’inde olduğu gibi engelin ebatlarını belirliyoruz. Burada Carve adında çok önemli bir değişken var. Dinamik engeller iki çeşit olabiliyor:

  • Carve destekleyen: Bu engeller NavMesh’te gördüğümüz mavi renkli temsili alanı, static objeler gibi keserek gerçek bir pathfinding deneyimi sunuyor.
  • Carve desteklemeyen: Bu engeller ise NavMesh’teki mavi renkli alana etki etmez. Bunların mantığı daha basittir: yapay zeka bu engel ile temas ederse engelin collider’ı boyunca hareket ederek onun etrafından dolanıp karşıya ulaşmaya çalışır. Yani eğer bu engel bir yolda karşıya geçişi tamamen engelliyorsa bile NavMesh sistemi bunu bilmediği için (çünkü engel carve desteklemiyor) yapay zeka, etrafından dolaşabileceğini ümit ederek engelin üzerine doğru hareket etmeye devam eder. Bu sistemin artı yanı carve’a göre daha hızlı olmasıdır çünkü NavMesh’i gerçek zamanlı olarak manipüle etmek (carve), sistemi biraz yoran bir iş. Ancak hareketli objeler için bu mod uygun bir seçenek olarak kabul edilebilir çünkü obje hareketli olduğu için mantıken kısa bir süre sonra yapay zekanın önünden çekilecektir.

Carve sistemi zahmetli olduğu için Unity bu sistemi optimize etmemize yarayan değişkenler sunmuş bize. Örneğin “Carve Only Stationary” seçeneği, carve işleminin (yani NavMesh’teki mavi alanı parçalama işleminin) sadece engel sabit iken yapılmasını sağlar ve çok da ideal bir seçenektir kendisi. “Time To Stationary” değişkeni ise engelin kaç saniye boyunca hareketsiz kalırsa carve yapacağını belirler (Carve Only Stationary seçili değilse bir önemi yoktur). Eğer Carve Only Stationary seçeneği aktif değilse de obje her “Move Threshold” birim kadar hareket ettiğinde carve işlemi uygulanır. Bizim yeşil küpümüz sadece hareketsiz iken, yani 90 derece dönmesini tamamladığı vakit carve yapacak (Carve Only Stationary seçili olacak).

Engeli kendi etrafında 90 derece döndürmek için “EngelDondur” adında yeni bir C# scripti oluşturup onu yeşil renkli engele verin:

using UnityEngine;

public class EngelDondur : MonoBehaviour 
{
	private Transform tr;

	// Objenin 90 derecelik dönüşü tamamlama süresi
	public float donmeSuresi = 0.35f;
	
	// Dönüşü donmeSuresi kadar sürede tamamlamak için
	// bir saniyede dönülmesi gereken açı miktarı
	private float saniyedeDonmeMiktari;

	// Objenin mevcut/hedef Y rotasyonları
	private float mevcutRotasyonY;
	private float hedefRotasyonY;

	void Start()
	{
		// Transform component'ini değişkende depola
		tr = transform;

		// Bir saniyede dönülmesi gereken açı miktarını hesapla
		saniyedeDonmeMiktari = 90f / donmeSuresi;

		// Mevcut/hedef eğimlere değer olarak objenin mevcut eğimini ver
		mevcutRotasyonY = tr.localEulerAngles.y;
		hedefRotasyonY = mevcutRotasyonY;
	}

	void Update()
	{
		// Ekrana sol mouse tuşu ile tıklanırsa
		// hedef eğimi 90 derece artır
		if( Input.GetMouseButtonDown(0) )
			hedefRotasyonY += 90f;
		
		// Eğer henüz dönme işlemi tamamlanmadıysa
		if( mevcutRotasyonY != hedefRotasyonY )
		{
			// Objeyi bir miktar döndür
			mevcutRotasyonY += saniyedeDonmeMiktari * Time.deltaTime;

			// Dönme işlemi tamamlandıysa eğimi tam olarak hedefRotasyonY'ye eşitle
			if( mevcutRotasyonY > hedefRotasyonY )
				mevcutRotasyonY = hedefRotasyonY;
			
			// Hesaplanan eğimi objeye uygula
			tr.localEulerAngles = new Vector3( 0f, mevcutRotasyonY, 0f );
		}
	}
}

Bu basit scriptin yaptığı şey ekrana sol tıklayınca engeli kendi etrafında 90 derece döndürmek.

Oyunu çalıştırıp engeli çevirirseniz ve bu esnada NavMesh’teki değişiklikleri Scene panelinden izlerseniz (Navigation paneli de açık olmak zorunda) aşağıdaki gibi bir görüntü ile karşılaşacaksınız:

17

Engel sabit iken carve yapıyor, yani mavi renkli alanı parçalıyor. Engel hareket ettiği vakit bu parçalama işlemi geri alınıyor (ta ki obje tekrar durana kadar). Engel tekrar durduğunda ise mevcut konum ve eğimine göre tekrar carve işlemine maruz kalıyor. Engel yolu tıkıyorken karşıya geçmek isteyince yapay zeka o yolu kullanamaz iken engel 90 derece dönmüş vaziyette iken yapay zeka, eğer o yol daha kısaysa o yolu tercih ediyor. Yapay zeka tam o yolu kullanacakken engeli tekrar döndürdüğümüzde (yolu kapatacak şekilde) yapay zeka güzergah değiştirerek mecburen uzun yolu kullanıyor. Yapay zeka karar değiştirdikten sonra yolu tekrar açtığımızda ise maalesef yapay zekanın güzergahı tekrar otomatik olarak güncellenmiyor; ancak hedef noktaya tekrar sağ tıklayarak manual olarak bir güncelleme talep edebiliyoruz.

Engeli seçip Inspector‘dan Carve seçeneğini kapatın. Artık ne durumda olursa olsun engel NavMesh’i carve etmeyecek (parçalamayacak):

18

Bu durumda yapay zekaya engelin olduğu yoldan karşıya geçmesini söylerseniz yapay zeka tıkanır. Her ne kadar yol üzerinde engel olsa da NavMesh, o yol üzerinde iki parçaya ayrılmadığı (carve) için yapay zeka o yolu müsait olarak algılar ve yeşil engele doğru hareket eder. Engelle temas etse bile gaza basmaya devam eden yapay zeka sonsuza kadar burada takılı kalır çünkü yol, engel tarafından tamamen kapatılmıştır.

Oyunu Yapmaya Başlayalım!

NavMesh’i az biraz tanıdık. Oyunumuzu yapmaya hazırız! Oyun alanımızı oluşturmadan önce sahnedeki ıvır zıvırdan kurtulalım. Hierarchy‘de sadece Main Camera ve Directional Light kalacak şekilde geri kalan her şeyi silin (eğer test sahnenizin silinmesini istemiyorsanız File-Save As diyerek sahneyi farklı kaydedebilirsiniz).

Karakteri Oluşturmak

İlk önce karakterimizi oluşturalım. Karakterimiz 3 küpten oluşacak. Sahnede üç adet küp oluşturup isimlerini Govde, Kol ve Kafa yapın. Ardından Transform component’lerini alttaki gibi elleyin:

  • Govde: position(0, 0, 0), scale(0.8, 1, 1)
  • Kol: position(0, 0, 0), scale(1.2, 0.2, 0.2)
  • Kafa: position(0, 0.7, 0), scale(0.4, 0.4, 0.6)

Project panelinde Create-Material yoluyla iki adet Material oluşturun:

  • KarakterAlt: Albedo rengi rgb(137, 221, 255)
  • KarakterUst: Albedo rengi rgb(201, 244, 255)

Govde objesine KarakterAlt materyalini, Kol ve Kafa’ya ise KarakterUst materyalini verin:

19

Oyunumuzun kamerası izometrik (orthographic) olacak, çünkü bence basit oyunlar için bu kamera daha şirin oluyor. Bunu Scene panelinde yapmak için sağ üstteki gizmo‘ların ortasındaki küpe tıklayın:

20

Artık karakter şöyle gözükecek:

21

Şimdi GameObject-Create Empty yoluyla sahnede boş bir obje oluşturun. Objeye “Player” ismini verip pozisyon ve rotasyonunu sıfırlayın. Karakterin 3 parçasını da Player’a child obje olarak atayın. Bunu yapıyoruz çünkü böylece Player objesini kullanarak tüm karakteri hareket ettirebileceğiz. Ayrıca animasyon vermemiz de daha kolay olacak. Hazır animasyondan laf açılmışken, neden karakterimize bir animasyon vermiyoruz ki!

Karaktere Animasyon Vermek

Player objesi seçili iken Window-Animation (Animator değil!) ile animasyon penceresini açın ve oradaki Create butonuna tıklayın. Animasyona isim olarak “KarakterHareket” verin. Şimdi timeline’ın ilk frame’ine (0:00) gelin. Kafa objesini seçin ve Record tuşuna basın:

22

Şu anda yaptığımız tüm ayarlar animasyonun 0:00 keyframe’ine kaydedilmekte. Kafa objesinin Transform‘undan position y değerini birazcık artırın (değeri önemli değil, oranın değerini değiştirince position kısmı kırmızı olacak; amacımız bu), ardından değeri geri 0.7‘e çekin. Bu şekilde Unity’e bu keyframe’de kafanın pozisyonunun y koordinatının 0.7 olacağını bildirdik:

23

Şimdi keyframe’i 0:20‘e getirin (bu, bir saniyenin 1/3’ü oluyor) ve kafanın y‘sini 0.85 yapın. Son olarak keyframe’i 0:40‘a getirip kafayı geri 0.7‘e taşıyın. Record butonunun sağındaki Play tuşuna basarsanız kafanın yukarı aşağı yaptığı bir animasyona sahip olduğumuzu göreceksiniz.

Benzer şekilde kollara da animasyon verelim. Keyframe’i tekrar en başa alın, Kol objesini seçin ve Rotation‘ı (0,0,0) olarak kaydedin (yine rotation’da herhangi bir değeri elleyip kırmızı yaptıktan sonra değerleri elle (0,0,0) yapabilirsiniz). 0:10 keyframe’inde rotation y‘yi 15 yapın (diğer değerler 0 kalabilir), 0:30‘da rotation y’yi -15 yapın ve son olarak 0:40‘ta rotation y’yi geri 0 yapın. Animasyonu oynatırsanız karakterin hem kafasını hem de kollarını oynattığını göreceksiniz:

23_1

Karakter hareket etmezken (idle) uzuvlarının hareket etmesini istemiyoruz. Bunun için ona hareketsiz olduğu yeni bir animasyon daha verecek ve karakter sabit iken bu animasyonu oynatacağız. Animation panelindeki “KarakterHareket” yazısına tıklayın ve “Create New Clip…” diyerek “KarakterSabit” adında yeni bir animasyon oluşturun. Bu animasyonun 0:00 keyframe’inde kafanın y pozisyonunu 0.7, kolun rotation‘ını ise (0,0,0) olarak belirleyin:

24

NOT: Animasyon yapmakla işiniz bitince Record tuşunu söndürüp Animation panelini kapatmak isteyebilirsiniz (sağ click-Close Tab) zira aksi taktirde Inspector’da kasıtsızca yaptığınız bir değişiklik animasyonunuza siz farkında olmadan kaydedilebilir ve sizi üzebilir.

Karakterimizin animasyonları hazır. Bu animasyonlar arası geçişi script yazarak ve Animator componentinden faydalanarak yapacağız. Biz Player objesine ilk animasyonumuzu verdiğimiz anda o objede otomatik olarak bir Animator component‘i de oluştu (ve de Project paneline Player adında bir Animator Controller asset’i geldi):

25

Animator component’indeki Player asset’ine çift tıklayarak (Mecanim) Animator panelini açın:

26

Şu anda KarakterHareket animasyonu turuncu renkte. Bunun anlamı, oyun başlayınca varsayılan olarak bu animasyonun oynayacağıdır. Halbuki biz bu animasyonu sadece karakter hareket ederken oynatmak istiyoruz. Bu yüzden KarakterSabit animasyonuna sağ tıklayıp “Set as Layer Default State” seçeneğini seçin. Artık KarakterSabit animasyonu turuncu renkli oldu. İki animasyon arasında geçiş yapmak için basit bir boolean parametre kullanacağız. Sol üstteki Parameters‘a tıklayıp + işareti ile “Hareket” isminde yeni bir Bool parametre oluşturun:

27

Karakter hareket ederken script vasıtası ile bu parametreyi true, karakter sabitken de false yapacağız. Geriye animasyonlar arası geçişi bu parametreye bağlamak kaldı. Bunun için ise KarakterSabit‘e sağ tıklayıp “Make Transition” deyin ve okun ucunu KarakterHareket‘in üzerinde bırakın. Oluşan okun üzerine bir kere tıklayarak onu seçili hale getirin ve Inspector’un en altındaki Conditions‘taki + butonuna tıklayarak oraya Hareket parametresini ekleyin. Beklenen değer olarak true verin:

28

Artık bu parametre true olduğunda karakterin hareket animasyonu oynayacak. Hareket animasyonundan geri sabit durma animasyonuna da dönüş yapabilmeliyiz. Bunun için KarakterHareket‘ten KarakterSabit‘e bir ok çizin ve okun koşulunu “Hareket = false” olarak belirleyin. Böylece Hareket koşulu geri false olduğunda animasyon da KarakterSabit’e dönecek. Bu ayarlamalara ilave olarak her iki ok için de Inspector‘dan “Has Exit Time” kutucuğunun başındaki işareti kaldırın; böylece iki animasyon arasındaki geçiş mümkün olan en kısa sürede (ama yine yumuşak bir şekilde) gerçekleşecek.

Hazır karakterimizi oluşturmuşken onun hareket scriptini de yazalım ve elimizin altında çalışan bir şey olsun diye düşünüyorum. Karakterimizin engellerin içinden geçmesini istemediğimiz için ona collider ve rigidbody (karakter hareketli bir obje olduğu için) vermemiz lazım. Şu anda kafa kol ve gövdenin her birinde birer Box Collider var zaten ama üç tane collider’a ihtiyacımız yok bizim. Bunun için Kol ve Kafa‘daki collider’ları “Remove Component” ile silin (component’in sağ üstündeki dişli çarkı kullanarak):

29

Ardından Govde‘deki collider’ı şu şekilde büyütelim:

30

Collider’larımız hazır olduğuna göre Player objesini seçin ve Component-Physics-Rigidbody ile ona bir Rigidbody verin. Govde objesinde rigidbody olmadığı için, gövde bir obje ile temas edince onun parent‘ı olan Player’ın rigidbody’sine collision event’i gidecek.

Zemin Oluşturmak

Karakteri hareket ettireceğiz ancak isterseniz önce karakterin üzerinde yürüyeceği bir zeminimiz olsun. Bunun için sahnede yeni bir küp oluşturup küpü (0,0,0) koordinatlarına taşıyın ve boyut (scale) olarak (10, 1, 10) değerini verin:

31

Zemini oluşturduktan sonra karakterimiz zeminin içinde kaldığı için karakteri Y ekseninde bir birim yukarı “(0, 1, 0)” taşıyın.

Zeminimiz şu anda beyaz ve çok sıradan duruyor. Ona bir renk verelim. Bunun için “Zemin” adında yeni bir materyal oluşturup Albedo rengini rgb(75, 195, 125) şeklinde güzel bir yeşil olarak belirleyin ve bu materyali zemin objesine atayın.

Büyük olasılıkla şu anda kameranız sahneye çok absürd bir konumdan bakıyordur. Kamerayı sahnenize izometrik (tepeden çapraz) bakacak şekilde konumlandırın/eğim verin. Bunun kısa yolu Scene panelindeki kamerayı, Main Camera‘nın olmasını istediğimiz noktaya getirmek, Main Camera objesini seçmek ve GameObject-Align With View yolunu izlemek. Scene panelindeki kameramız izometrik ancak Main Camera’mız değil (şu anda perspektif). Bu durumu düzeltmek için Main Camera’nın Projection değişkenini Orthographic olarak değiştirin:

32

Artık kameranızın sahneye şuna benzer şekilde baktığınız varsayıyorum:

33

Karakteri Hareket Ettirmek

En nihayetinde gelelim karakteri hareket ettirmeye (oh be!). “KarakterHareket” adında yeni bir C# scripti oluşturup içeriğini şöyle değiştirin:

using UnityEngine;

public class KarakterHareket : MonoBehaviour 
{
	private Transform tr;
	private Rigidbody rb;
	private Animator anim;
	
	private float horizontalInput = 0;
	private float verticalInput = 0;
	
	private Vector3 kameraForward;
	private Vector3 kameraRight;
	
	public float hareketHizi = 3f;
	public float donmeHizi = 360f;
	
	void Start()
	{
		// Player'ın sahip olduğu önemli component'leri değişkenlerde depola
		tr = transform;
		rb = GetComponent<Rigidbody>();
		anim = GetComponent<Animator>();
		
		// Kameraya göre ileri (forward) ve sağ (right) yönleri hesapla
		// Bu vektörleri, karakteri kameranın baktığı yönde hareket
		// ettirmek için kullanacağız
		kameraForward = Camera.main.transform.forward;
		kameraRight = Camera.main.transform.right;
		
		// Kameranın aşağı-yukarı eğimini yoksay
		kameraForward.y = 0f;
		kameraRight.y = 0f;
		
		// Yön vektörlerinin uzunluklarının 1 olmasını sağla
		kameraForward.Normalize();
		kameraRight.Normalize();
	}
	
	void Update()
	{
		// Klavyeden input al
		horizontalInput = Input.GetAxisRaw( "Horizontal" );
		verticalInput = Input.GetAxisRaw( "Vertical" );
	}
	
	void FixedUpdate()
	{
		// Eğer hareket tuşlarından en az birine basılıyorsa
		if( horizontalInput != 0 || verticalInput != 0 )
		{
			// Animator'un Hareket parametresini true yaparak
			// KarakterHareket animasyonunun oynamasını sağla
			anim.SetBool( "Hareket", true );
			
			// Karakterin hareket edeceği yönü hesapla
			Vector3 hareketYonu = ( kameraForward * verticalInput + kameraRight * horizontalInput ).normalized * hareketHizi * Time.deltaTime;
			
			// Karakteri hareket yönüne doğru yumuşak bir şekilde döndür
			Quaternion hedefRotation = Quaternion.LookRotation( hareketYonu, Vector3.up );
			tr.rotation = Quaternion.RotateTowards( tr.rotation, hedefRotation, donmeHizi * Time.deltaTime );
			
			// Karakteri rigidbody vasıtasıyla hareket ettir
			rb.MovePosition( rb.position + hareketYonu );
		}
		else
		{
			// Eğer hareket tuşlarından hiçbirine basılmıyorsa Animator'un
			// Hareket parametresini false yaparak KarakterSabit
			// animasyonunun oynamasını sağla
			anim.SetBool( "Hareket", false );
		}
	}
}

Bu script birazcık uzun ama olur o kadar da 🙂 Scriptin önemli noktalarından bahsetmek istiyorum. Örneğin Start fonksiyonunda kameranın ileri ve sağ yönlerini hesaplamak işlemine bakalım. Klavyeden ileri ok tuşuna basınca mantıken karakterin tam olarak kameranın baktığı yönde hareket etmesini bekleriz, çapraz falan hareket etmesini değil. Bunun için karakteri hareket ettirirken kameranın yön vektörlerinden faydalanmalıyız. Sadece iki vektör işimizi görmeye yetiyor: kameranın ileri yönü ve kameranın sağ yönü. Main Camera‘ya erişmek için Camera.main komutunu kullanıyoruz. Bu komut sahnedeki “MainCameratag‘ına sahip olan kameranın Camera component‘ini döndürür. Bizse bu objenin transform‘una erişip oradan da forward (ileri) ve right (sağ) yönlerini çekiyoruz. Bu vektörlerin Y koordinatını sıfırlıyoruz çünkü biz XZ düzleminde hareket ettiğimiz için hareket vektörümüzün Y’sinin 0 olmasını bekleriz. Son olarak da yön vektörleri üzerinde Normalize fonksiyonunu çağırıyoruz. Bu fonksiyon, vektörün uzunluğunu 1 yapmaya yarar. Bunu istiyoruz çünkü yön vektörlerini hangi değerle çarparsak karakterin tam olarak o çarptığımız değer kadar hızlı gitmesini istiyoruz (aksi taktirde karakterin hızını gerçek anlamda belirlemek mümkün olmaz çünkü yön vektörünün gereğinden fazla uzun veya kısa durumda olması riski var).

Scriptte hem Update hem de FixedUpdate fonksiyonlarından faydalandım. Unity’nin video derslerinde bolca gördüğüm birşeydi bu. Eğer obje fizik (rigidbody) ile hareket edecek ise Update fonksiyonunda input‘ları alıyor, FixedUpdate fonksiyonunda ise hareket kodunu yazıyoruz. Ben de orada gördüğümden esinlenerek böyle bir yola başvurdum.

Scriptin can alıcı noktası FixedUpdate fonksiyonu çünkü tüm önemli işleri burada yapıyoruz. Öncelikle oyuncunun herhangi bir hareket tuşuna o anda basmakta olup olmadığını kontrol ediyor ve ona göre Animator‘un “Hareket” parametresinin değerini veriyoruz (bir tuşa basılıyorsa true, yoksa false). Böylece karakterin hareket animasyonu ile sabit durma animasyonu arasında script vasıtasıyla geçiş yapmış oluyoruz. Eğer bir hareket tuşuna basılıyorsa karakterin gitmesi gereken yönü hesaplıyoruz. Bunun için kameraForward (ileri hareket yönü) vektörü ile ileri-geri hareket input’unu (verticalInput) çarpıyor, kameraRight (sağ hareket yönü) vektörü ile de sağ-sol hareket input’unu (horizontalInput) çarpıyoruz ve bu iki vektörü birbiri ile toplayarak karakterin aynı anda ileri-geri ve sağ-sol yönlerde hareket edebilmesini sağlıyoruz. Toplama işleminin sonucunda elde ettiğimiz vektörün sonuna neden “.normalized” yazdık diyebilirsiniz. Bir vektörün sonuna “.normalized” yazarsanız o vektörün uzunluğunun 1 olduğu versiyonu döndürülür. Bunu yapıyoruz çünkü karakterin hem ileri hem sağa giderken (çapraz giderken) bir hız boost’u kazanmasını istemiyoruz (yani matematiksel olarak, tek bir yönde giderkenki hızından daha hızlı gitmesini istemiyoruz). Bulduğumuz XZ yön vektörünü normalized ederek bu boost’un olmasını engelliyoruz. Elde ettiğimiz vektörü ise “hareketHizi * Time.deltaTime” ile çarparak bu yönde saniyede hareketHizi kadar birim gitmeyi sağlıyoruz. Hareket etme işlemini ise rigidbody‘nin MovePosition fonksiyonu ile yapıyoruz. Tek yaptığımız, rigidbody’nin mevcut konumundan hareketYonu kadar uzaktaki noktayı fonksiyona parametre olarak vermek.

MovePosition fonksiyonunu ben de daha yeni yeni öğrendim (unity video dersleri sağolsun, gerçekten çok faydalılar). Normalde rigidbody‘nin velocity‘sini ellemeye çalışıyor veya AddForce ile güç uyguluyor, sonra karakterin çok fazla hızlanmasını çözmekle uğraşıyordum. Ancak MovePosition imdadıma yetişti. Bu fonksiyonun bir avantajı da fizik olaylarını dikkate alması. Eğer transform.Translate fonksiyonu kullansaydık ya da direkt transform.position‘ı değiştirseydik fizik olayları anında dikkate alınmazdı. Örneğin önümüzde bir duvar olsa ve duvara doğru Translate etsek karakter önce duvarın içine girer, ardından fizik motoru devreye girerek karakteri duvarın dışına çıkarır ve ekranda takılmalı çirkin bir görüntü oluşurdu. MovePosition kullanınca ise karakterin duvarın içine girmesi gibi bir sıkıntı olmuyor çünkü fizik motoru böyle birşey olacak gibiyse duruma anında müdahale ediyor.

FixedUpdate‘in içinde karakterin rotation‘ını değiştirdiğim iki satırlık bir kod da var. Bir ihtimal bu kod gözünüzü korkutmuştur çünkü içerisinde, ismi bile yeterince korkunç olan Quaternion‘lar var (uuu çok korkunç). Biz her ne kadar Inspector‘da objenin eğimini xyz olarak görsek de aslında bu eğim bir Quaternion olarak depolanmakta. Quaternion’lar tam olarak nasıl çalışıyorlar açıkçası ben de bilmiyorum. Sadece eğim verirken işime yarayabilecek fonksiyonları ezberlemeye çalışıyorum. Kodda kullandığım LookRotation fonksiyonu, bir objeye hangi rotation değerini verirsek onun forward ekseninin (ileri yön, mavi ok) hareketYonu vektörü yönünde olacağını ve up (yukarı, yeşil ok) ekseninin de Vector3.up vektörü yönünde olacağını bize döndürüyor. Karakter hareketYonu’nde hareket ettiği için onun yüzünün de (forward) bu yöne doğru bakmasını istiyoruz doğal olarak. Karakterin up vektörünün ise yukarı bakmasını istiyoruz zira aksi taktirde karakter tepetaklak hareket eder. LookRotation’ın döndürdüğü değeri hemen transform‘un rotation‘ına değer olarak vermiyoruz çünkü böyle yaparsak karakterin eğimi, biz ok tuşları arasında gidip geldikçe küt diye değişir durur, yumuşak bir animasyon olmaz (kısacası çirkin durur). Karakterin yumuşak bir şekilde dönmesi için ise RotateTowards fonksiyonunu kullanıyoruz. Bu fonksiyon, bir rotation’dan başka bir rotation’a doğru donmeHizi kadar yaklaşmayı sağlar. Bizde üçüncü parametre “donmeHizi * Time.deltaTime” olduğu için bizim bu yumuşak animasyonumuz saniyede donmeHizi kadar açısal hıza sahip oluyor.

Bu yeterince sıkıcı kodu az biraz anlatmayı bitirdiğime göre eğlenceli kısma gelelim ve oyunu test edelim (hâlâ test etmediyseniz tabi ki). Player objesine KarakterHareket scriptini component olarak verip oyunu çalıştırın. Karakteri WASD tuşları ile düzgün bir şekilde hareket ettirebilmeniz lazım ve bu esnada da karakterin doğru yönde dönmesi lazım:

33_1

Düşmanı Oluşturmak

NOT: Oyundaki rakibimize yazı boyunca düşman diye sesleniyorum çünkü örnek oyun başta bir futbol oyunu olmayacak ancak savaşmalı bir oyun olacaktı. Daha sonradan futbol oyununa dönüş yapmaya karar verdim.

Artık düşmanları oluşturmaya hazırız. Bu oyunda tek tip düşmanımız olacak ve o da bizim karakterimize oldukça benzeyecek (ama onun kadar obur olmayacak). Player objesini seçin ve CTRL+D veya Edit-Duplicate ile onu klonlayın. Klon objenin ismini “Dusman” olarak değiştirin. Düşman objeyi (2, 1, 0) pozisyonuna taşıyın.

Düşmanı değiştirmeye kilosundan başlayalım. Dusman‘ın Govde‘sini seçin ve Scale değerini (0.7, 1, 0.5) olarak değiştirin. Ardından DusmanAlt isminde yeni bir materyal oluşturup Albedo rengini rgb(255, 70, 70) olarak değiştirin ve bu materyali Dusman’ın Govde’sine verin:

34

Eh, bence düşman objemiz kabaca hazır. Düşmanı NavMesh ile hareket ettireceğimiz için Dusman objesindeki Rigidbody‘deki “Is Kinematic” seçeneğini işaretleyin. Böylece NavMesh ile fizik motoru arasında bir kavga çıkmayacak. Ardından Dusman objesine Component-Navigation-Nav Mesh Agent component’i verin ve değerleri şöyle değiştirin:

35

Agent Size‘da Dusman‘ı Scene panelinde çevreleyen silindirin ebatlarını uygun şekilde değiştiriyoruz. Düşmanın bizden biraz daha yavaş olması için hızını 2 veriyor ve açısal hızını saniyede 360 derece olarak belirliyoruz. Ben performans için “Obstacle Avoidance“ı “Medium Quality“e çekmeyi düşündüm ancak bu tercih size kalmış.

Düşmanı Hareket Ettirmek

Bence oluşturduğumuz bu düşmana bizi kovalamasına yarayan bir script vererek aksiyonu yavaştan başlatalım. İlk iş Dusman‘daki “Karakter Hareket” component’ini Remove Component diyerek yok edin. Ardından “DusmanHareket” adında yeni bir C# script oluşturun:

using UnityEngine;

public class DusmanHareket : MonoBehaviour 
{
	private NavMeshAgent yapayZeka;
	private Transform player;
	
	void Start()
	{
		// Düşmanın sık kullandığımız NavMeshAgent component'ini bir değişkene at
		yapayZeka = GetComponent<NavMeshAgent>();
		
		// Player objesini (yani bizi) bir değişkene at
		player = GameObject.FindWithTag( "Player" ).transform;
		
		// Animator'un "Hareket" parametresini true yap
		// Düşman hiç durmayacağı için bu bize bir sıkıntı oluşturmayacak
		GetComponent<Animator>().SetBool( "Hareket", true );
	}
	
	void Update()
	{
		// Düşmanın rotasını güncelle (hedef=biz)
		yapayZeka.destination = player.position;
	}
}

Bu kısacık scriptin Update fonksiyonu sayesinde düşmanın hedefine sürekli bizim mevcut pozisyonumuzu koyuyoruz. Böylece düşman sürekli bizi kovalıyor. Player objesini sahnede bulmak için ise GameObject.FindWithTag fonksiyonunu kullanıyoruz. Bu fonksiyon, GameObject.Find fonksiyonuna göre çok daha hızlıdır ve objelerin ismine göre değil de tag‘ine göre arama yapar. Parametre olarak “Player” girmişiz, o halde Player objemizin tag’ini Player yapmamız lazım. Bunun için Hierarchy‘den Player‘ı seçin ve Inspector‘dan objeye Player tag‘ını verin:

36

Script hazır. Geriye iki şey kalıyor: scripti Dusman objesine component olarak vermek (lütfen yapınız) ve sahnenin şu anki hali için NavMesh oluşturmak. O halde hiç durmayın ve Navigation‘daki Bake butonuna abanın (ama önce zemin objesini Static yapın):

36_1

Şimdi oyunu çalıştırırsanız düşmanın bizi sürekli takip ettiğini görebilirsiniz. Eğer biz hareket etmezsek düşmandaki rigidbody bizim rigidbody’mizi iterek sahneden dışarı fırlatabiliyor:

37

Bence şu haliyle bile elimizde eğlenceli bir konsept var. Ama durmak yok, yola devam!

Kameranın Player’ı Takip Etmesi

Şu anda kameramız sabit olduğu için kolayca kameranın görüş alanının dışına çıkabiliyoruz. Bunu çözmenin en iyi yolu ise kameraya, oyuncuyu takip etmeye yarayan bir script vermek. “KameraHareket” adında yeni bir C# script oluşturalım:

using UnityEngine;

public class KameraHareket : MonoBehaviour 
{
	private Transform tr;
	private Transform player;
	
	// Kamera ile player arasındaki uzaklık vektörü
	private Vector3 deltaVektor;
	
	void Start()
	{
		tr = transform;
		player = GameObject.FindWithTag( "Player" ).transform;
		
		// Kameranın oyuncunun bulunduğu konumdan
		// 25 birim uzakta olması için gerekli uzaklık vektörü
		deltaVektor = -tr.forward * 25f; 
		
		// Kamerayı, player'ı tam ortalayacak şekilde konumlandır
		tr.position = player.position + deltaVektor;
	}
	
	// Karakter FixedUpdate'te hareket ettiği için kamera hareketi de burada olmalı
	void FixedUpdate()
	{
		// Kameranın oyuncuyu yumuşak bir şekilde takip etmesini sağla
		tr.position = Vector3.Lerp( tr.position, player.position + deltaVektor, 0.075f );
	}
}

Burada tek ilginç şey deltaVektor değişkeni. Eğer kameranın pozisyonunu direkt Player‘ın pozisyonuna eşitlersek, kamera player’ın içinde kalacağı için hiçbir şey göremeyiz. Böyle bir sorun yaşamamak için kamerayı, player’ın deltaVektor kadar gerisine çekiyoruz (25 birim).

FixedUpdate fonksiyonunda basit bir Lerp fonksiyonu ile kamera takip işlemini yumuşatıyoruz. Üçüncü parametreyi (0.075f) ne kadar azaltırsak kamera, player’ı o kadar yavaş takip eder. Eğer bu parametreyi 1.0f yaparsak kameranın yumuşak takip etmesi tarihe karışır.

Scripti Main Camera objesine verip oyunu test edin. Kameranın yumuşak bir şekilde karakteri takip etmesi lazım.

Karakteri Eğimli Yüzeyde Hareket Ettirmek

Artık temel şeyler hazır gibi duruyor. Peki gerçekten öyle mi? Karakteri hiç eğimli bir yüzeyde hareket ettirmeyi denemedik. Örneğin sahnedeki zemin objesini klonlayıp Rotation Z değerini 30 yapın. Ardından vertex snap kullanarak bu eğimli zemini öteki zeminin ucuna yerleştirin. Bunun nasıl olduğunu bilmiyorsanız basitçe anlatayım: Translate tool‘u (W) seçiyorsunuz. Scene paneli açık iken eğimli zemini seçiyorsunuz ve V tuşuna basılı tutuyorsunuz. Fare imlecini objenin herhangi bir köşesine getirdiğinizde Translate gizmo’su oraya zıplıyor:

37_1

Üstteki gif animasyonda da gördüğünüz üzere V basılı iken köşeden tutup başka köşeye fare imlecini sürükleyince o iki köşe birleşiyor. Böylece iki objeyi tam anlamıyla uç uca yerleştirmek mümkün oluyor. Siz de resimde olduğu gibi eğimli yüzeyi düz yüzeyin yanına yapıştırın. Ardından oyunu çalıştırıp eğimli yüzeye çıkmaya çalışın. Maalesef ki mümkün değil çünkü yerçekimi etkisi ile küpümüz aşağı güçlüce çekiliyor. Bu sorunu çözmek için iki yol önereceğim; bu iki yolun birbirlerine göre avantajlarını/dezavantajlarını birlikte göreceğiz.

Yol 1: Rigidbody’i Kısıtlamak

Tek yapmanız gereken Player objesinin Rigidbody‘sindeki tüm Rotation Constraint‘lerini işaretlemek:

38

Fizik motorunun Player objesinin eğimini değiştirmesine engel oluyoruz. Böylece yer çekiminin objemiz üzerindeki gücü azalıyor çünkü Player’ı eğimli yüzeyde aşağı doğru yuvarlayabilme yetkisini fizik motorunun elinden alıyoruz (ancak eğer eğimli yüzeyin eğimini abartırsak [mesela 60 derece] o zaman yerçekimi bizi yenmeyi başarıyor). Eğer Navigation panelinden Bake yaparsanız düşmanın sizi yokuşta takip etmekte bir sıkıntı yaşamadığını göreceksiniz; neyse ki onda ekstra ayar gerekmiyor (Nav Mesh Agent herşeyi hallediyor bizim için).

Bu yolu ben tercih etmeyeceğim çünkü nedense karakterin fizik motoru tarafından etrafta yuvarlanabilmesi benim hoşuma gidiyor. Sırf bunun için de bir ton emek sarf edip ikinci yola başvuracağım. Şimdiden özür diliyorum 😉

Yol 2: Biraz Daha Kod Yazmak

Eğer Player‘ın Rotation‘ını önceki yolda kısıtladıysanız şimdi bu değişikliği geri alın ve alttaki resme bakın:

39

Biz objemizi şekil A‘daki gibi yere çapraz olarak yokuştan çıkarmaya çalışınca fizik motoru doğal olarak olaya müdahale ediyor. O halde neden fizik motorunun sözünü dinleyerek şekil B‘deki gibi yere paralel gitmeyelim ki! Bunu başarmak için KarakterHareket scriptine biraz ekleme yapmamız gerekiyor:

using UnityEngine;

public class KarakterHareket : MonoBehaviour 
{
	... // Önceki değişkenler aynen kalacak
	
	// Eğimli yüzeylerde gitmek için kullanacağımız Raycast'in
	// hangi objelere uygulanacağını belirleyen mask değişkeni
	public LayerMask zeminLayer;
	
	void Start() {
		... // değişiklik yok
	}
	
	void Update() {
		... // değişiklik yok
	}
	
	void FixedUpdate()
	{
		// Eğer hareket tuşlarından en az birine basılıyorsa
		if( horizontalInput != 0 || verticalInput != 0 )
		{
			// Animator'un Hareket parametresini true yaparak
			// KarakterHareket animasyonunun oynamasını sağla
			anim.SetBool( "Hareket", true );
			
			// Karakterin hareket edeceği yönü hesapla
			Vector3 hareketYonu = ( kameraForward * verticalInput + kameraRight * horizontalInput ).normalized * hareketHizi * Time.deltaTime;
			Vector3 yukariYon = Vector3.up;
			
			// Karakterin merkez noktasından aşağı yönde 2 birim uzunluğunda
			// raycast yap (sadece zeminLayer layer'ındaki objeleri dikkate al)
			RaycastHit hit;
			if( Physics.Raycast( tr.position, Vector3.down, out hit, 2f, zeminLayer ) )
			{
				// Eğer raycast'imiz bir zemin objesi ile temas etmişse
				// (kısacası karakter havada uçmuyorsa)
				// karakterin yukarı eksenini (yeşil ok) temas ettiğimiz zeminin
				// normal vektörü olarak belirle ve hareketYonu vektörünün
				// temas ettiğimiz düzlem üzerindeki izdüşümünü bulup bu vektörü
				// hareketYonu değişkenine geri yolla
				yukariYon = hit.normal;
				hareketYonu = Vector3.ProjectOnPlane( hareketYonu, hit.normal );
			}
			
			// Karakteri hareket yönüne doğru yumuşak bir şekilde döndür
			Quaternion hedefRotation = Quaternion.LookRotation( hareketYonu, yukariYon );
			tr.rotation = Quaternion.RotateTowards( tr.rotation, hedefRotation, donmeHizi * Time.deltaTime );
			
			// Karakteri rigidbody vasıtasıyla hareket ettir
			rb.MovePosition( rb.position + hareketYonu );
		}
		else
		{
			// Eğer hareket tuşlarından hiçbirine basılmıyorsa Animator'un
			// Hareket parametresini false yaparak KarakterSabit
			// animasyonunun oynamasını sağla
			anim.SetBool( "Hareket", false );
		}
	}
}

Kod uzun olduğu için değişiklik yapmadığım kısımları (değişken tanımlamaları, Start ve Update fonksiyonu) kestim. Yeni eklediğimiz değişkene bakmadan önce FixedUpdate‘teki değişikliklere bakalım. Buraya birkaç satır daha kod ekledik. “hareketYonu” değişkenimizi normal bir şekilde hesapladıktan sonra yukariYon adında bir vektör oluşturuyoruz; varsayılan değeri (0, 1, 0). Bunu görselle açıklamaya çalışacağım.

40

yukariYon” değişkenimiz resimde görülen “Düzlemin normal vektörü“nü, yani zeminden Player objesinin kafasına doğru yöne sahip olan vektörü depoluyor (bir başka deyişle karakterin yukarı yönünü depoluyor). yukariYon’ü tanımladıktan hemen sonra Physics.Raycast ile, karakterin merkezinden çıkıp dimdik aşağı yönde maksimum 2 birim giden ve sadece zeminLayer‘a sahip objelerle temas edebilen bir raycast ışını yolluyoruz. Bu raycast ışını oyun boyunca hemen her zaman bir şeye temas ediyor olacak. Ya üzerinde düz ilerlediğimiz zemine temas edecek ya da tırmanmakta olduğumuz yokuşa. Mesela üstteki resimde yokuş üzerinde seyahat etmekteyiz. Raycast birşeye temas etmiyorsa büyük olasılıkla haritanın sınırlarından aşağı düşmekle meşgulüzdür.

Raycast bir obje ile temas ederse bu temas hakkında detaylı bilgi tercihen RaycastHit türündeki bir değişkene atanır (bizde bu değişkenin ismi hit) ve “if(Physics.Raycast(…))” koşulu true döndürür (if‘in içine girilir). Eğer ki bir zemine temas ediyorsak yukariYon‘e değer olarak zeminin normal vektörünü (resimdeki “Düzlemin normal vektörü“) veriyoruz. Bu vektör zemine diktir ve uzunluğu 1 birimdir (daima). Son olarak da düz zemine paralel olan hareketYonu vektörümüzün yokuş üzerindeki izdüşümünü alıyoruz, yani vektörün yokuşa paralel versiyonunu buluyoruz (resimdeki mor renkli vektör). Bu vektörün boyunun hareketYonu’nden kısa olduğuna dikkat edin; çünkü izdüşüm ile sadece vektörün “işe yarar” kısmını elde ediyoruz, yere dik olan “gereksiz” kısmından kurtuluyoruz. Bir vektörün bir düzlem üzerindeki izdüşümünü bulmak için Vector3.ProjectOnPlane fonksiyonu kullanılır. Birinci parametreye izdüşümü alınacak vektör girilirken ikinci parametreye ise düzlemin normal vektörü girilir (böylece uzayda sonsuz uzunluğa sahip olan düzlemin en azından ne yönde baktığı tespit edilir).

Bu azıcık karmaşık uzay geometrisinden kurtulduktan sonra hedefRotation‘ı bulmaya yarayan LookRotation fonksiyonunun ikinci parametresini Vector3.up‘tan yukariYon‘e değiştiriyoruz (bu değişkeni süs olsun diye oluşturmadık herhalde). Scriptin en tepesindeki yeni değişkene dönecek olursak; bu LayerMask değişkeni içerisinde bir dizi layer tutmaya yarar ve bu değişken vasıtası ile bir Raycast‘e hangi layer’daki objeleri dikkate alması gerektiğini söyleyebiliriz. Her layer’a raycast yapmamak performans açısından önemli birşey, hakeza Raycast ışınının uzunluğunu mümkün olduğunca kısa tutmak da öyle.

Şu anda yapmamız gereken şey raycast‘in sadece zemin objeleri üzerinde etkili olması için zemin objelerine yeni bir layer vermek. Bunun için herhangi bir zemin objesini seçip Inspector‘un tepesinden “Layer-Add Layer…” yolunu izleyin. Gelen panelde boş bir yere “Zemin” layer’ı ekleyin:

41

Ardından her iki zemin objesini de Inspector‘dan “Layer-Zemin” yolunu izleyerek Zemin layer‘ına atayın.

42

Geriye sadece bir şey kaldı: Player objesini seçip Karakter Hareket component‘indeki “Zemin Layer“a değer olarak Zemin layer’ını verin:

43

Oyunu başlatın. Karakter artık hangi zeminde hareket ediyorsa o zemine paralel gidiyor. Ancak bir başka sorun var şimdi de…

44

Düşman, bizim aksimize, hâlâ yokuşa çapraz olarak hareket ediyor. Burada eğer isterseniz düşman objesini böyle bırakabilirsiniz. Ama yok ben düşmanın da bizim gibi düzgün hareket etmesini istiyorum diyen tayfadansanız (ben) o zaman daha yapacaklarımız bitmedi. Yine biraz kod yazacağız. DusmanHareket scriptini şöyle düzenleyin:

using UnityEngine;

public class DusmanHareket : MonoBehaviour 
{
	private NavMeshAgent yapayZeka;
	private Transform tr;
	private Rigidbody rb;
	private Transform player;
	
	public LayerMask zeminLayer;
	
	void Start()
	{
		// Düşmanın sık kullandığımız NavMeshAgent component'ini bir değişkene at
		yapayZeka = GetComponent<NavMeshAgent>();
		tr = transform;
		rb = GetComponent<Rigidbody>();
		
		// Player objesini (yani bizi) bir değişkene at
		player = GameObject.FindWithTag( "Player" ).transform;
		
		// Animator'un "Hareket" parametresini true yap
		// Düşman hiç durmayacağı için bu bize bir sıkıntı oluşturmayacak
		GetComponent<Animator>().SetBool( "Hareket", true );
		
		// Düşmanın konumunun NavMesh tarafından otomatik olarak güncellenmesini engelle
		// (yani adım atılacak konumu hesapla ama o adımı atma)
		yapayZeka.updatePosition = false; 
		
		// Düşmanın eğiminin (rotation) NavMesh tarafından otomatik olarak
		// değişmesini engelle, artık eğimi kendimiz bulacağız
		yapayZeka.updateRotation = false;
	}
	
	void Update()
	{
		// Düşmanın rotasını güncelle (hedef=biz)
		yapayZeka.destination = player.position;
		
		// Düşmanın updatePosition'ı açık olsaydı bu frame'de
		// atacağı adımın yönünü hesapla
		Vector3 yapayZekaAdim = yapayZeka.nextPosition;
		Vector3 hareketYonu = yapayZekaAdim - tr.position;
		Vector3 yukariYon = Vector3.up;
		
		// KarakterHareket scriptinde yaptığımız işlemin birebir aynısı
		RaycastHit hit;
		if( Physics.Raycast( tr.position, Vector3.down, out hit, 2f, zeminLayer ) )
		{
			yukariYon = hit.normal;
			hareketYonu = Vector3.ProjectOnPlane( hareketYonu, hit.normal );
		}
		
		// Düşmanı hareket yönüne doğru yumuşak bir şekilde döndür
		Quaternion hedefRotation = Quaternion.LookRotation( hareketYonu, yukariYon );
		tr.rotation = Quaternion.RotateTowards( tr.rotation, hedefRotation, 360f * Time.deltaTime );
		
		// Düşmanı, NavMesh'in istediği yere hareket ettir
		rb.MovePosition( yapayZekaAdim );
	}
}

En başa düşmanın Transform‘unu ve Rigidbody’sini depolayan yeni değişkenler (tr ve rb) ve bir LayerMask değişkeni ekledik. Start fonksiyonunun en sonunda NavMeshAgent component’inin updatePosition değerini false yaptık. Bunun anlamı şu: NavMesh sistemi normal bir şekilde düşmanın o frame’de nereye hareket etmesi gerektiğini hesaplayacak ancak düşmanı hareket ettirmeyecek. Düşmanı daha sonra o noktaya biz hareket ettireceğiz. Eğer düşman hareket etmeden önce ona birşeyler yaptırmak istiyorsanız (düzgünce döndürmek) bu değişkeni false yapmak ideal bir çözümdür. Benzer şekilde updateRotation‘ı da false yapıyoruz. Eğer yapay zekanın eğimini elle belirlemek istiyorsanız (scripti güncellememizin nihai amacı) bu değişken de false olmak zorunda ki yapay zekanın rotation‘ı NavMeshAgent tarafından belirlenmesin.

Update fonksiyonunda yapayZekaAdim adında bir değişkene NavMeshAgent‘ın nextPosition değerini aktarıyoruz. İşte bu nextPosition değeri, eğer updatePosition true olsaydı, bu frame’de yapay zekanın hareket edeceği koordinatı depoluyor. Bu koordinattan yapay zekanın mevcut pozisyonunu çıkararak düşmanın o frame’de gitmek istediği yönü buluyoruz. Ardından KarakterHareket‘teki birebir aynı mantıkla düşmanın eğimli düzlemdeki hareket yönünü ve yukarı yönü düzgün bir şekilde hesaplıyoruz. Düşmana eğimini verdikten sonra da onu rigidbody‘sinin MovePosition fonksiyonu ile yapayZekaAdim‘a, yani yapay zekanın gitmesini istediği noktaya hareket ettiriyoruz.

Dusman objesinin Inspector‘undan Zemin Layer‘a değer olarak Zemin verip oyunu çalıştırın:

44_1

Karakteri yokuştan çıkarabilmek için yaptığımız bu 2. yol meyvesini verdi ve bence karakterin (ve düşmanın) yokuşta rotasyon değiştirmesi güzel de bir görüntü oluşturdu. Burada tek dezavantaj ise, artık fizik motorunun karakteri yuvarlama yetkisi olduğu için 1. yola nazaran daha az dik yokuşları çıkabilmemiz (örneğin 45 derecelik yokuş çıkmak mümkün değil).

NOT: Bu safhada oyunu çalıştırdığınızda konsolda bazen şu uyarıyı görebilirsiniz:

45

Bu uyarının demek istediği şey şu: DusmanHareket scriptindeki LookRotation fonksiyonuna parametre olarak girdiğimiz hareketYonu değişkeninin değeri (0,0,0). LookRotation bir yön vektörü bekliyor ancak biz ona bir yöne sahip olmayan tek vektörü, yani Vector3.zero‘yu değer olarak verince bu uyarıyı alıyoruz. Peki hareketYonu düşmanda ne zaman 0 olur? Yapay zeka o frame’de kendi bildiği bir sebepten ötürü hareket etmemeyi tercih ederse. Bu durumda hem bu uyarıyı almaktan kurtulmak hem de düşman hareket etmediği vakit tüm o Quaternion ile eğim verme olaylarından kurtulmak mümkün. Tek yapmanız gereken Quaternion.LookRotation fonksiyonunun olduğu satırın üzerindeki boş satıra şu kodu eklemek:

// Eğer düşman bu frame'de hareket etmek istemiyorsa
// uyarı almaktan ve Quaternion'lar ile çalışmaktan bu frame'e mahsus kurtul
if( hareketYonu == Vector3.zero )
	return;

NOT2: Oyunu test ederken karakterin bazı yokuşlara (mesela 30 derecelik) dosdoğru değil de çapraz olarak giriş yaparken zaman zaman başarısız olduğunu gördüm. Bunun sebebi; Raycast‘teki tr.position noktası, yani karakterin orta noktası yokuşun üzerine gelip yokuşla paralel olma işlemini başlatana kadar karakterin hızı fizik motoru tarafından yeniliyor (karakter çapraz olduğu için) ve karakterin orta noktası yokuşun üzerine gelemiyor. Velhasıl bu sorunu çözmek için KarakterHareket‘teki Raycast‘in ilk parametresini “tr.position + tr.forward * 0.25f” şeklinde değiştirerek karakterin orta noktası değil de daha öne yakın bir noktası (0.25 birim) yokuşa girince paralelleşe işlemini başlatmak mümkün. İlaveten, oyunun aksiyonunu biraz daha artırmak için player’ın hızını 4, düşmanın hızını ise 3.5 olaracak şekilde artırdım.

Sahneyi Oluşturmak

Karakter de düşman da hazır duruyor. Artık basit bir sahne hazırlayabiliriz. Ben şunun gibi basit bir sahne oluşturmayı uygun gördüm:

46

Gördüğünüz gibi sahnede 1 düz zemin, 4 yatay zemin ve 4 de çapraz zemin var. Siz kendi sahnenizi dilediğinizce yapabilirsiniz; benim sahnem için ayarlar şöyle:

DuzZemin: scale(10, 1, 10)

EgikZemin: position(6.55, 0.82, 0), rotation(30, 270, 0), scale(10, 1, 3)

EgikZemin: position(0, 0.82, -6.55), rotation(30, 0, 0), scale(10, 1, 3)

EgikZemin: position(-6.55, 0.82, 0), rotation(30, 90, 0), scale(10, 1, 3)

EgikZemin: position(0, 0.82, 6.55), rotation(30, 180, 0), scale(10, 1, 3)

CaprazZemin: position(5.558, 0.82, -5.558), rotation(30, 315, 0), scale(3.675, 1, 3)

CaprazZemin: position(-5.558, 0.82, -5.558), rotation(30, 45, 0), scale(3.675, 1, 3)

CaprazZemin: position(-5.558, 0.82, 5.558), rotation(30, 135, 0), scale(3.675, 1, 3)

CaprazZemin: position(5.558, 0.82, 5.558), rotation(30, 225, 0), scale(3.675, 1, 3)

Player: position(0, 1, -4)

Dusman: position(0, 1, 4), rotation(0, 180, 0)

Main Camera: rotation(45, 45, 0)

Tahmin edebileceğiniz üzere sahnemi vertex snapping kullanarak ayarladım, bu ayarları elle girerek bulmak olur iş değil çünkü. Tüm zemin objelerinizin layer‘ını “Zemin” yapın. Materyallerini de Zemin materyali yapmayı unutmayın. Sonrasında ise Navigation‘dan Bake yaparak NavMesh alanını oluşturun:

47

Arenanın kenarlarından aşağı düşmek istemiyorsanız her bir kenar için görünmez (Mesh Renderer’ı kapalı) bir küp oluşturabilirsiniz:

EgikZeminCollider: position(8.1, 4.5, 0), rotation(0, 0, 0), scale(1, 5, 10)

EgikZeminCollider: position(-8.1, 4.5, 0), rotation(0, 0, 0), scale(1, 5, 10)

EgikZeminCollider: position(0, 4.5, 8.1), rotation(0, 90, 0), scale(1, 5, 10)

EgikZeminCollider: position(0, 4.5, -8.1), rotation(0, 90, 0), scale(1, 5, 10)

CaprazZeminCollider: position(6.65, 4.5, -6.65), rotation(0, 45, 0), scale(1, 5, 3.675)

CaprazZeminCollider: position(-6.65, 4.5, -6.65), rotation(0, 135, 0), scale(1, 5, 3.675)

CaprazZeminCollider: position(-6.65, 4.5, 6.65), rotation(0, 225, 0), scale(1, 5, 3.675)

CaprazZeminCollider: position(6.65, 4.5, 6.65), rotation(0, 315, 0), scale(1, 5, 3.675)

48

Topu Oluşturmak

Gelelim asıl eğlenceye, yani oyunu oynayacağımız top objesine. Bunun için GameObject-3D Object-Sphere ile sahnede yeni bir küre oluşturup isim olarak “Top” verin. Scale değerini (0.5, 0.5, 0.5) yapın. Topa bir Rigidbody component’i verin ve oradaki Mass (Ağırlık) değerini 0.1‘e indirin. Böylece topu hareket ettirmek çok daha kolay olacak.

Çevremizde gördüğümüz topların ortak bir özelliği az biraz sekmeleri. Bizim top objemizin de sekmesini ayarlayabiliriz. Bunun için Project panelinden Create-Physics Material yoluyla “Top” adında yeni bir Physics Material oluşturup değerlerini şöyle belirleyin:

49

Friction değerlerini azaltarak topa etkiyen sürtünmeyi azaltıyor ve böylece topun daha zor yavaşlamasını sağlıyoruz. Bounciness değerini artırarak ise topun sekme gücünü artırıyoruz. Bu Physics Material‘ı topa atamak için tek yapmanız gereken Top objesinin “Sphere Collider” component’indeki Material‘a sürükle-bırak yapmak:

50

Oyunu çalıştırıp topa abanırsanız onu sahada hareket ettirebilir ancak sahanın dışına çıkaramazsınız (collider’lar sağolsun).

İsterseniz topa da güzel bir materyal atayalım. “Top” adında yeni bir materyal oluşturup bu materyali topa verin. Ardından materyalin özelliklerini gönlünüzce değiştirin (ben Albedo rengini rgb(234, 231, 112) yaptım ve Smoothness değerini sona dayadım; aksi taktirde top çok parlıyordu).

Bu zamana kadar Dusman objesi temsili olarak hep bizi kovalıyordu. Artık onun da topu kovalama vakti geldi. Neyse ki bu çook kolay: top objesine yeni bir tag vermeli ve Dusman’ın hedefine topu atamalıyız.

NOT: Düşmanın bizi kovalamasını sağlamayı gördünüz. Bu bilgiler ışığında şu anda bile basit bir zombie survival oyunu yapabileceğinize inanıyorum (dalga dalga zombilerin geldiği türden).

Top‘u seçip Inspector‘un tepesinden “Tag-Add Tag…” yolunu izleyin ve “Top” adında yeni bir tag oluşturun (oradaki + butonuna basarak):

51

Tekrar top objesine gelip tag‘ini “Top” olarak belirleyin. Sonrasında DusmanHareket‘in Start fonksiyonundaki ilgili satırı “player = GameObject.FindWithTag( “Top” ).transform;” şeklinde değiştirin; yani tag’ı Player’dan Top’a çekin. Artık oyunu çalıştırınca Dusman objesi de Top’u kovalayacak.

Topa Vurmak

Şu anda topu sürükleyebiliyoruz ancak bence topa vurabilsek ayrı bir güzel olurdu. Bunun için topla temas ettiğimiz vakit ona ekstra güç uygulamamız lazım. Collider‘larında “Is Trigger“ı işaretli olmayan iki objenin teması esnasında, temas eden objelerin scriptlerinde OnCollisionEnter adında bir fonksiyon çalışır. Bu fonksiyon Collision türünde bir parametre alır; bu parametre ise temas edilen objeyi, temas noktasını, objelerin göreceli hızını vb. bir takım bilgileri depolar.

KarakterHareket scriptine şu fonksiyonu ekleyin:

void OnCollisionEnter( Collision temas )
{
	// Eğer temas edilen objenin tag'ı "Top" ise
	if( temas.gameObject.CompareTag( "Top" ) )
	{
		// Top ile karakter arasındaki yön vektörünü bul
		Vector3 yon = temas.transform.position - tr.position;
		
		// Topla karakter arasındaki açıyı hesapla
		float aci = Vector3.Angle( tr.forward, yon );
		
		// Eğer açı 45 dereceden küçükse topa, karakterin
		// hareket yönünde 1 birimlik güç uygula
		if( aci <= 45f )
			temas.gameObject.GetComponent<Rigidbody>().AddForce( tr.forward * 1.0f, ForceMode.Impulse );
	}
}

Karakter bir obje ile temas edince temas edilen objenin tag‘inin “Top” olup olmadığına bakıyoruz. Burada (tag == “Top”) yerine “CompareTag( “Top” )” kullandım çünkü duyduğuma göre bu fonksiyonu çalıştırmak biraz daha hızlı bir işlemmiş. Eğer topla temas ediyorsak top ile karakter arasındaki yön vektörünü buluyorum. Ardından bu yön vektörü ile karakterin ileri yönü (forward) arasındaki açıyı Vector3.Angle fonksiyonu ile hesaplıyorum. Bu açı 45 dereceden küçükse o halde topa maksimum 45 derecelik bir açıyla bakıyoruzdur (yani topa önümüz bakıyordur, arkamız veya yanımız değil). Bu durumda top objesine hareket yönümüzde 1.0f şiddetinde bir güç uyguluyoruz (AddForce ile). Fonksiyona girdiğimiz ikinci parametre olan ForceMode.Impulse şeysi topa anlık bir kuvvet uyguladığımızı belirliyor (sürekli olarak uygulanan bir kuvvetin aksine); böyle bir durumda fonksiyona girdiğimiz güç miktarı daha bir etkili oluyor:

52

Kameranın Topun Konumunu Dikkate Alması

Şu anda kamera sadece karakteri takip ediyor. Eğer top bir uca biz bir uca gidersek topu görmemiz zorlaşıyor. Eğer kameranın top ile karakterin orta noktasını takip etmesini ayarlarsak bu sorun ortadan kalkar; ayrıca kamera top ile birlikte sürekli hareket halinde olacağı için daha bir “dinamik” hissettirir.

Tahmin edeceğiniz üzere KameraHareket scriptini düzenliyoruz:

using UnityEngine;

public class KameraHareket : MonoBehaviour 
{
	...

	private Transform top;
	
	void Start()
	{
		...
		
		top = GameObject.FindWithTag( "Top" ).transform;
	}
	
	// Karakter FixedUpdate'te hareket ettiği için kamera hareketi de burada olmalı
	void FixedUpdate()
	{
		// Kameranın oyuncuyu ve topu yumuşak bir şekilde takip etmesini sağla
		// (ikisinin orta noktasını hedef alarak)
		tr.position = Vector3.Lerp( tr.position, ( player.position + top.position ) / 2 + deltaVektor, 0.075f );
	}
}

Önceki değişkenlere ek olarak, topun Transform‘unu depolayan yeni bir değişken ekleyip değerini Start fonksiyonunda verdik. FixedUpdate fonksiyonunda da hedef olarak player’ın pozisyonunu vermek yerine “(player.position + top.position) / 2” diyerek top ve player’ın orta noktasını verdik. Oyunu çalıştırırsanız farkı göreceksiniz.

Topu Ne Kadar Süre Ayağımızda Tuttuğumuzu Hesaplamak

Oyunun amacı topu 1 dakika boyunca ayağımızda tutabilmek. O halde bu süreyi bir zahmet hesaplayalım ki oyunu oynayabilelim. Topu hem biz hem de düşman kontrol edebiliyor. Her ikimiz için de süre tutmalıyız. Bu süre tutma işlemini ayrı bir scriptte yapmayı uygun gördüm ben. Bunun için de “OyunKontrol” adında basit bir C# script yazdım:

using UnityEngine;

// Topun mevcut sahibini ifade ederken kullanabileceğimiz bir enum veri türü
// Örneğin topun sahibi Player ise bunu TopSahibi.Player şeklinde ifade edebiliriz
public enum TopSahibi { Player, Dusman, Kimse };

public class OyunKontrol : MonoBehaviour 
{
	// Bu scripte kolayca erişebilmek için static bir instance değişken
	public static OyunKontrol instance = null;
	
	// Topun mevcut sahibi
	private TopSahibi topSahibi = TopSahibi.Kimse;
	
	// Topa en son dokunan kişi
	private TopSahibi topaSonDegenKisi = TopSahibi.Kimse;
	
	// Her iki oyuncunun topu kontrol etme süreleri
	private float playerTopSure = 0f;
	private float dusmanTopSure = 0f;
	
	void Awake()
	{
		// static instance değişkene değer olarak 
		// bu instance'ı (this) ver
		instance = this;
	}
	
	void Update()
	{
		// Topun sahibi kim ise onun süresini artır
		if( topSahibi == TopSahibi.Player )
			playerTopSure += Time.deltaTime;
		else if( topSahibi == TopSahibi.Dusman )
			dusmanTopSure += Time.deltaTime;
	}
	
	// Bir oyuncu topa değince çağrılan fonksiyon
	// topSahibi değişkeni hemen değiştirilmiyor çünkü
	// her iki oyuncunun da topa sürekli değdiği kargaşa
	// durumlarında topSahibi'nin ikide bir değer değiştirmesini
	// istemiyoruz
	public void TopaDegdim( TopSahibi degenKisi )
	{
		// Topa değen son kişiyi değişkene kaydet
		topaSonDegenKisi = degenKisi;
		
		// 0.2 saniye sonra TopSahibiniDegistir fonksiyonunu çağır
		CancelInvoke();
		Invoke( "TopSahibiniDegistir", 0.2f );
	}
	
	private void TopSahibiniDegistir()
	{
		// Topa son değen kişiyi topun sahibi yap
		topSahibi = topaSonDegenKisi;
	}
	
	void OnGUI()
	{
		// Her iki oyuncunun da sürelerini test amaçlı ekranın sol üst köşesinde yazdır
		GUILayout.Box( playerTopSure + " --- " + dusmanTopSure );
	}
}

Scriptin en başında bir enum veri türü tanımladım. Bu enum’u kullanan değişkenlerin alabileceği 3 değer var: TopSahibi.Player, TopSahibi.Dusman veya TopSahibi.Kimse. Topun sahibini bu class’a örneğin “Player” diye string olarak aktarmak yerine TopSahibi.Player olarak aktarmayı uygun gördüm çünkü string olarak yanlışlıkla “plAyer” gibi yazım hatası yaparsak bir compiler error almayız, ancak kod da düzgün çalışmaz ve biz hatayı bulmak için boş yere vakit kaybedebiliriz. Enum’larda yazım hatası yaparsak ise (TopSahibi.plAyer gibi) Unity konsola bir compiler error verir ve böylece birşeyleri yanlış yaptığımızı anlarız.

Class’ın içerisinde bir tane static değişken var: instance. Bu instance değişkeni Awake fonksiyonunda değerini alıyor (instance = this). Awake dediğimiz fonksiyon Start ile neredeyse birebir aynı; en önemli farkı Awake fonksiyonunun Start’tan daha önce çalıştırılması. Ben static değişkenlere Awake’te değer vermeyi tercih ediyorum. Static değişkenlerin güzel bir özelliği, bu değişkenlere dışarıdan çok kolayca erişilebilmesidir. Static değişkenleri Inspector‘da göremezsiniz çünkü static değişkenler objeye değil class’a aittir. Bir başka deyişle, elinizde A scripti varsa, bu scriptte de stc adında static bir değişken varsa ve bu A scriptini 5 tane objeye component olarak verdiyseniz bu 5 tane A component’inin her birinin stc değişkeninin değeri birebir aynıdır. Bir component’te stc’nin değerini değiştirince diğer 4 component’teki değer de değişir. Bu stc değişkenine ise herhangi bir scriptten kolayca A.stc şeklinde (classAdı.değişkenAdı) erişebiliriz; yani gidip de “GameObject.Find(“Obje”).GetComponent<A>().değişkenAdı) yazmak zorunda değiliz. Biz Awake’te “instance = this” dediğimiz zaman, Awake’i çalışmakta olan OyunKontrol component‘ini instance’a değer olarak veriyoruz. Böylece bu component’e başka bir scriptten “OyunKontrol.instance” şeklinde hızlıca erişebiliriz. Örneğin bu scriptte yer alan TopaDegdim fonksiyonunu başka bir scriptten çağırmak için OyunKontrol.instance.TopaDegdim(…) yazmamız yeterli oluyor.

TopSahibi enum‘u türünde iki değişkenimiz var. Birisi topun mevcut sahibini tutarken öbürü topa en son dokunan kişiyi tutuyor. Bu ikisi arasındaki fark şu: topun mevcut sahibi kimse onun süresi artar, topa en son dokunan kişi kim ise de topun yeni potansiyel sahibi odur. Topa en son dokunan kişiyi direkt topun sahibi yapmadım çünkü düşmanın elinden topu almaya çalışırken bazen kargaşa oluyor, top arada kalıp her ikimize de defalarca çarpıyor. Bu gibi durumlarda topun sahibinin player’dan düşmana, düşmandan player’a zıplayıp durmasını istemedim. Topun sahibi olabilmek için koyduğum şart ise basit: eğer topa dokunmuşsak ve biz topa dokunduktan sonra 0.2 saniye boyunca düşman topa dokunamamışsa o zaman topun sahibi biziz. Aynı durum düşman için de geçerli.

Update fonksiyonunda topun sahibi kimse onun süresini artırıyoruz. TopaDegdim fonksiyonunda ise topun yeni potansiyel sahibini, degenKisi parametresi ile belirliyoruz. Topa biri değdikten 0.2 saniye sonra TopSahibiniDegistir fonksiyonunu çağırıyoruz (bu fonksiyonun tek yaptığı, topa son dokunan kişiyi topun sahibi belirlemek). Bir fonksiyonu belli bir süre sonra çağırmak istiyorsanız Invoke kullanabilirsiniz. Ben Invoke’tan önce CancelInvoke da kullandım. CancelInvoke fonksiyonu, o component’te o an var olan tüm Invoke taleplerini silmeye yarar. Bu fonksiyonu çağırıyorum çünkü biz topa değmeden önce 0.2 saniyelik süre içerisinde başkası topa değmişse onun Invoke talebini yok ederek onun top sahibi olmasını engelliyoruz (artık topa biz dokunduğumuz için en güncel potansiyel top sahibi biziz).

OnGUI fonksiyonunu test amaçlı yazdım. Oyun çalışınca ekranın sol üstünde player’ın ve düşmanın sürelerini yazdırmaya yarıyor.

OyunKontrol scriptini Main Camera objesine verin ve KarakterKontrol scriptinin OnCollisionEnter fonksiyonunun en sonuna (if koşulunun içine) şu satırı ekleyin:

// Topun potansiyel sahibini Player olarak belirle
OyunKontrol.instance.TopaDegdim( TopSahibi.Player );

Sonra DusmanHareket scriptinin en sonuna şu fonksiyonu ekleyin:

void OnCollisionEnter( Collision temas )
{
	// Eğer temas edilen objenin tag'ı "Top" ise
	if( temas.gameObject.CompareTag( "Top" ) )
	{
		// Topun potansiyel sahibini Dusman olarak belirle
		OyunKontrol.instance.TopaDegdim( TopSahibi.Dusman );
	}
}

Artık Dusman objesinin de OnCollisionEnter fonksiyonu var. Temas edilen obje Top ise topun yeni potansiyel sahibini Dusman olarak belirliyoruz.

Sayaçlara Arayüz Oluşturmak

Artık sayaçlarımız tamamdır! Şu anda test amaçlı kullandığımız çirkin OnGUI arayüzü haricinde bir sıkıntımız yok. Ancak bence Unity’nin yeni UI sisteminden faydalanarak daha güzel bir sayaç arayüzü oluştursak daha iyi olur. Benim aklımdaki şey şu: topun sahibi kimse onun kafasının üzerinde o kişinin sayacı gözüksün. Top el değiştirince eski sahibin sayacı yok olup yeni sahibinki görünür olsun.

Player için sayaç oluşturmaya başlayalım. GameObject-UI-Text yoluyla yeni bir text (yazı) objesi oluşturun. Hierarchy‘de bir canvas, canvas’ın içerisinde bir text ve bir de event system objesi oluşacak. Bu derste UI sisteminin detaylarına inmeyeceğim; eğer hiç bilginiz yoksa UI dersimi okumak isteyebilirsiniz: https://yasirkula.com/2015/01/21/unity-ui-arayuz-sistemi/

Biz sayacı topun sahibinin kafasında, yani 3D uzayda göstermek istiyoruz. Bunun için Canvas objesindeki Canvas component‘inin “Render Mode“unu “World Space” yapın:

53

Şu anda canvas devasa birşey; onu küçültmemiz lazım. Bunun için onun RectTransform‘unu biraz küçültelim:

54

Rotation X ve Y değerlerini kameramınki ile aynı yaptım, yani 45 derece. Böylece Canvas‘ın içindeki UI elemanları (bizim durumumuzda sadece Text) doğrudan kameraya doğru bakıyorlar ve olabilecek en net şekilde görünüyorlar.

Şimdi Text objesini seçin. Text’in Canvas’ın tamamını kaplayabilmesini sağlayalım (şu anda maksimum 160×30’luk bir alan kaplayabiliyor). Bunu yapmak epey kolay: Pos X ve Width değerlerinin solundaki kare şeklindeki ikona tıklıyor, ALT tuşuna basılı tutuyor ve menünün en sağ altındaki seçeneğe tıklıyoruz:

54_1

Şimdi de Text component’ini kullanarak yazıyı canvas’ın içerisinde ortalayalım ve Best Fit ile font boyutunun otomatik olarak ayarlanmasını sağlayalım:

55

Yazının rengini (Color) ise Player‘ın materyal rengi, yani rgb(137, 221, 255) yapalım. Yazının rahat gözükmesi için de ona Component-UI-Effects-Outline ile bir dış hat ekleyelim. Son olarak da Text objesinin ismini “PlayerSayac” olarak değiştirelim.

Şimdi düşmanın sayacını yapalım. PlayerSayac objesini seçip CTRL+D ile klonlayın (duplicate) ve yeni objenin ismini “DusmanSayac” yapın. DusmanSayac’ın Text component’inin Color (renk) değerini rgb(255, 70, 70) yapın. İşte bu kadar!

Sayaçları otomatik olarak konumlandırmayı ve “New Text” yazısını düzenleme işini OyunKontrol scriptinde yapacağız:

using UnityEngine;
using UnityEngine.UI;

...

public class OyunKontrol : MonoBehaviour 
{
	...
	
	public Text playerSayac;
	public Text dusmanSayac;
	
	private Transform playerSayacTransform;
	private Transform dusmanSayacTransform;
	
	private Transform playerTransform;
	private Transform dusmanTransform;
	
	void Awake() {
		...
	}
	
	void Start()
	{
		// Her iki sayacın da görünürlüğünü (alpha) sıfırla
		Color c = playerSayac.color;
		c.a = 0f;
		playerSayac.color = c;
		
		c = dusmanSayac.color;
		c.a = 0f;
		dusmanSayac.color = c;
		
		// Sayaçların Transform component'lerini değişkenlerde depola
		playerSayacTransform = playerSayac.transform;
		dusmanSayacTransform = dusmanSayac.transform;
		
		playerTransform = GameObject.FindWithTag( "Player" ).transform;
		dusmanTransform = GameObject.Find( "Dusman" ).transform;
	}
	
	void Update()
	{
		// Topun sahibi kim ise onun süresini artır
		if( topSahibi == TopSahibi.Player )
		{
			playerTopSure += Time.deltaTime;
			
			// Player'ın sayacının görünürlüğünü (alpha) artır
			Color c = playerSayac.color;
			c.a += 10f * Time.deltaTime;
			c.a = Mathf.Clamp( c.a, 0f, 1f );
			playerSayac.color = c;
			
			// Düşmanın sayacının görünürlüğünü azalt
			c = dusmanSayac.color;
			c.a -= 10f * Time.deltaTime;
			c.a = Mathf.Clamp( c.a, 0f, 1f );
			dusmanSayac.color = c;
			
			// Player'ın sayacını güncelle (virgülden sonra sadece 1 basamak bastır)
			playerSayac.text = playerTopSure.ToString( "F1" );
		}
		else if( topSahibi == TopSahibi.Dusman )
		{
			dusmanTopSure += Time.deltaTime;
			
			// Player'ın sayacının görünürlüğünü (alpha) azalt
			Color c = playerSayac.color;
			c.a -= 10f * Time.deltaTime;
			c.a = Mathf.Clamp( c.a, 0f, 1f );
			playerSayac.color = c;
			
			// Düşmanın sayacının görünürlüğünü artır
			c = dusmanSayac.color;
			c.a += 10f * Time.deltaTime;
			c.a = Mathf.Clamp( c.a, 0f, 1f );
			dusmanSayac.color = c;
			
			// Düşmanın sayacını güncelle (virgülden sonra sadece 1 basamak bastır)
			dusmanSayac.text = dusmanTopSure.ToString( "F1" );
		}
	}
	
	void LateUpdate()
	{
		// Sayaçların konumlarını ayarla (hedef pozisyondan
		// 2 birim yukarı koy ki sayaç hedefin içinde kalmasın)
		playerSayacTransform.position = playerTransform.position + Vector3.up * 2f;
		dusmanSayacTransform.position = dusmanTransform.position + Vector3.up * 2f;
	}
	
	...
	
	// Artık OnGUI'ye ihtiyacımız yok
	/*void OnGUI()
	{
		...
	}*/
}

En başa “using UnityEngine.UI;” yazıyoruz çünkü Unity’nin UI sistemine kodla erişmek için bu şart. Start fonksiyonunda her iki sayacı da, renklerinin alpha (a) değerini 0 yaparak görünmez hale getiriyoruz. Dusman objesine erişmek için GameObject.Find fonksiyonunu kullanıyoruz çünkü Dusman’ın ayırt edici bir tag‘ı yok (aslında ona da tag verip kodu düzenlerseniz daha güzel olur).

Update fonksiyonu çok daha kalabalık duruyor ama aslında hâlâ oldukça basit. Top kimde ise onun sayacının alpha‘sını artırıyor, ötekininkini azaltıyoruz. Böylece topun sahibinin sayacı yumuşak bir şekilde görünür hale gelirken ötekinin sayacı yumuşak bir şekilde görünmez oluyor. Alpha değeri 0 ile 1 arasında bir değer almak zorunda (0=görünmez, 1=tamamen görünür) ve bu yüzden Mathf.Clamp fonksiyonu ile alpha’ların 0 ile 1 arasında olmasını garantiliyoruz. Son olarak da top sahibinin sayacını güncelliyoruz. Burada “ToString(“F1”)” komutunu kullanıyoruz. Bu komut float bir sayıyı, virgülden sonra sadece 1 basamağını alarak string’e çevirir.

LateUpdate fonksiyonunun tek yaptığıysa sayaçların pozisyonlarını player ve düşman objelerinin merkez noktalarının 2 birim yukarısı olarak ayarlamak. Eğer 2 birim yukarı koymazsak sayaçlar player ve düşmanın içinde kalır. Neden LateUpdate kullanıyoruz? Çünkü Update fonksiyonunda player ve düşmanın konumları güncellendikten sonra sayaçların konumunu güncellemek istiyoruz. Yani sayaçları, player ve düşmanın bir önceki frame’deki konumuna değil de mevcut frame’deki en güncel konumuna taşımak istiyoruz (LateUpdate fonksiyonu Update’ten sonra çalışır).

Son olarak da artık OnGUI fonksiyonunu comment‘leyerek (veya silerek) ondan kurtuluyoruz.

Main Camera‘daki Oyun Kontrol component‘inde yer alan sayaç değişkenlerine değerlerini verin:

56

Şimdi oyunu çalıştırıp sayaçların kafalarda düzgün bir şekilde gözüktüğünü teyit edebilirsiniz (aynı anda sadece bir sayaç görünür olmalı):

57

NOT: İsterseniz sayaçların fontunu değiştirebilirsiniz. Windows’ta fontlar “C:/Windows/Fonts” klasöründe tutuluyor. Buradan beğendiğiniz bir fontu, Unity’nin Project paneline sürükle-bırak yapıp ardından sayaçların Text component’inin Font kısmına değer olarak verebilirsiniz. Ben Moire fontunu tercih ettim.

Oyunun Zorluk Dozajını Biraz Artırmak

Şu anda bile oyun bazen zorlayıcı olabiliyor. Ufak hilelerle bu dozajı daha da artırabiliriz. Örneğin Dusman objesinin “Nav Mesh Agent“ındaki “Auto Braking“i kapatırsanız düşman topa yakınlaştıkça yavaşlamaz. İlaveten topun hakimiyetini biz (player) ele geçirirse düşmanın hızını biraz artırarak bize kolayca yetişebilmesini sağlayabiliriz; düşman top hakimiyetini geri kazanırsa hızını tekrar düşürebiliriz. Bunu yapmak epey kolay aslında. OyunKontrol scriptinde 2 yeni değişken tanımlayalım:

public float dusmanMaksimumHiz = 4.25f;
public float dusmanMinimumHiz = 3.5f;

Ardından TopSahibiniDegistir fonksiyonunun sonuna şu kodu ekleyelim:

// Düşman objesinin hızını, topa sahip olup olmamasına göre artır/azalt
if( topSahibi == TopSahibi.Dusman )
	dusmanTransform.GetComponent<NavMeshAgent>().speed = dusmanMinimumHiz;
else
	dusmanTransform.GetComponent<NavMeshAgent>().speed = dusmanMaksimumHiz;

Kodun açıklamaya gerek kalmadan anlaşıldığını düşünüyorum.

Aklıma gelen bir diğer nokta ise düşmanın hareketine biraz daha bilinmezlik katmak. Şu anda düşmanı kendi başına bırakırsak topu dümdüz ileri sürüyor. Düşmanı arada biraz sağa ya da sola meyletmek için ise DusmanHareket scriptinin OnCollisionEnter fonksiyonundaki TopaDegdim satırından sonra şu kodu yazabiliriz:

// Topa yatay eksende rastgele bir güç uygulayarak düşmanın arada bir
// sağa ya da sola meyletmesini sağla
temas.rigidbody.AddForce( tr.right * Random.Range( -5f, 5f ) );

Yaptığımız şey, düşman topa her dokunduğunda topa düşmanın yatay (sağ-sol) ekseninde maksimum 5 birimlik rastgele bir güç uygulamak. Top yatay eksende hafif hareket edince düşman da onu takip ederken hafif sağa-sola dönecek. Oyunu çalıştırırsanız farkı görebilirsiniz zaten.

NOT: Bazen top aşırı bir güç etkisiyle sahanın kenarlarındaki görünmez duvarlara çarpıp çok fena yükseliyor ve en sonunda da sahanın dışına düşüp oyunu oynanmaz kılıyor (ben iki kere yaşadım). Bunu engellemek için sahanın tepesine büyükçe bir collider eklemek isteyebilirsiniz (pos(0, 8, 0), rotation(0,0,0), scale(16, 2, 16)).

NOT2: Eğer Scene panelindeki “New Text” yazılı Text objeleri oyunu düzenlerken sizi de yeterince rahatsız ediyorsa onların Text component‘lerinin Text değişkenini boş bir string yapabilirsiniz.

Sahaya Dinamik Engeller Koymak – Pervane

Dersin başlarında NavMesh‘i dinamik engeller ile parçalamayı gördük ama o gün bu gündür daha da dinamik engellerin yüzünü görmedik. Ben izninizle bu duruma bir dur demek ve sahaya oyunu zorlaştırıcı ve daha eğlenceli kılıcı birkaç dinamik engel eklemek istiyorum. Engellerin arada bir belirip kısa bir süre sahada durup sonra kaybolmasını uygun buldum, aksi taktirde oyuncuyu sıkabiliriz.

İlk engelimiz sahanın tam ortasında yer alacak olan bir pervane olacak. Bu pervane yerden yavaşça belirecek, bir süre yüksek hızda dönecek ve ardından geri kaybolacak. Önce pervanemiz için iki tane küp oluşturun:

PervaneMerkez: position(0, 0.75, 0), scale(0.75, 1.5, 0.75)

PervaneUc: position(0, 1, 0), scale(7, 0.4, 0.4 )

Engel” adında yeni bir materyal oluşturup bu materyali her iki küpe de atayın. Bende materyalin rengi rgb(255, 255, 200).

Ardından GameObject-Create Empty ile “Pervane” adında yeni bir GameObject oluşturup az önceki iki objeyi de bu objenin birer child‘ı yapın (öncesinde Pervane’yi position(0,0,0)’a taşıyın). Ardından Pervane‘ye bir Rigidbody ekleyip “Is Kinematic“i işaretleyin. İlaveten objeye bir de Nav Mesh Obstacle component’i ekleyin:

58

Geriye pervanenin animasyonlarını yapmak kaldı. Belirme ve kaybolma animasyonlarını Animator kullanarak hallederken dönme animasyonunu kodla halledeceğiz (daha rahatıma geldi böyle). Önce belirme animasyonundan başlayalım.

Window-Animation ile Animation panelini açın. Pervane objesi seçili iken Create butonuna basarak “PervaneBeliris” adında yeni bir animasyon oluşturun. Animasyon şöyle birşey:

0:00 – position = (0, -1.1, 0)

0:40 – position = (0, 0.25, 0)

1:10 – position = (0, 0, 0)

1:30 – position = (0, 0, 0)

Pervane önce haddinden fazla yükselip (0:40) sonra hemen durması gelen yüksekliğe iniş yapıyor (1:10) ve kısa bir süre bu yükseklikte sabit kalıyor (1:10 – 1:30). Şimdi Pervane objesi için “PervaneKapanis” adında yeni bir animasyon daha oluşturun:

0:00 – position = (0, 0, 0)
0:30 – position = (0, -1.1, 0)

Bu basit animasyonu da oluşturduk. Animasyonları Project panelinden tek tek seçip her biri için “Loop Time” değerini kapatın. Loop Time açık olduğu zaman animasyon kendisini sonsuza kadar tekrar eder. Biz animasyonun sadece 1 kere oynamasını istiyoruz.

59

Sonrasında Pervane objesini seçip Animator component’indeki “Update Mode“u “Animate Physics” yapın. Böyle yaptığınız vakit animasyon, fizik motoru ile uyumlu bir şekilde oynar ve örneğin pervane belirirken pervanenin tam üzerinde Player var idiyse Player da pervane ile birlikte yükselir:

60

Şimdiyse “Pervane” isimli Animator Controller‘a çift tıklayarak Animator‘u açın ve “Parameters“a “Kapan” isminde yeni bir Trigger parametre ekleyin:

61

Trigger parametresi Bool‘a çok benzer. Aralarındaki fark şu: Trigger tek bir frame için true olur ve ardından otomatik olarak false olur. Eğer bir animasyondan başka bir animasyona tek yönlü geçiş yapacaksanız (geri dönmeyecekseniz) Trigger idealdir.

Animator‘da PervaneBeliris‘ten PervaneKapanis‘a bir transition yapıp Condition olarak Kapan trigger‘ını verin. “Has Exit Time“ın işaretini kaldırın. Bence artık pervane için script yazmaya hazırız. O halde “Pervane” adında yeni bir C# script oluşturalım:

using UnityEngine;
using System.Collections;

public class Pervane : MonoBehaviour 
{
	private Rigidbody rb;
	
	// Pervanenin maksimum dönüş hızı ve bu hıza ulaşmak
	// için sahip olduğu açısal ivmesi
	public float maksimumDonusHizi = 135f;
	public float ivme = 80f;
	
	// 1= ivmelendir
	// 0= ivme yok
	// -1= yavaşlat
	private int ivmeKatsayi = 0;
	
	// Pervanenin oyunda durma süresi
	public float pervaneMinimumSure = 15f;
	public float pervaneMaksimumSure = 20f;
	
	// Pervanenin mevcut dönüş hızı ve eğimi
	private float mevcutDonusHizi = 0f;
	private Vector3 egim = Vector3.zero;

	void Start()
	{
		rb = GetComponent<Rigidbody>();
	}
	
	// OnEnable, obje her active edildiğinde çalıştırılır
	void OnEnable()
	{
		ivmeKatsayi = 0;
		
		// PervaneBaslat fonksiyonunu coroutine olarak başlat
		StopAllCoroutines();
		StartCoroutine( PervaneBaslat() );
	}
	
	// Coroutine'lerin return type'ı IEnumerator'dur
	// ve System.Collections'ı import etmek gereklidir
	private IEnumerator PervaneBaslat()
	{
		// Pervane için rastgele bir çalışma süresi bul
		float pervaneSure = Random.Range( pervaneMinimumSure, pervaneMaksimumSure );
		
		// Pervanenin maksimumHiz'dan tamamen durmasına kadar geçen süreyi hesapla
		float durmaSuresi = maksimumDonusHizi / ivme;
		
		Animator anim = GetComponent<Animator>();
		
		// 1.5 saniye bekle (PervaneBeliris'in süresi)
		yield return new WaitForSeconds( 1.5f );
		
		// Pervaneyi ivmelendir
		anim.enabled = false;
		ivmeKatsayi = 1;
		
		// Pervanenin yeterince dönmesini bekle
		yield return new WaitForSeconds( pervaneSure );
		
		// Pervaneyi yavaşlat
		ivmeKatsayi = -1;
		
		// Pervane durana kadar bekle
		yield return new WaitForSeconds( durmaSuresi );
		
		// PervaneKapanis animasyonunu oynat
		anim.enabled = true;
		anim.SetTrigger( "Kapan" );
		
		// 0.5 saniye bekle (PervaneKapanis'in süresi)
		yield return new WaitForSeconds( 0.5f );
		
		// Pervane objesini inaktif yap
		gameObject.SetActive( false );
	}
	
	void FixedUpdate() 
	{
		// Mevcut hızı hesapla
		mevcutDonusHizi += ivme * ivmeKatsayi * Time.deltaTime;
		mevcutDonusHizi = Mathf.Clamp( mevcutDonusHizi, 0f, maksimumDonusHizi );
		
		// Pervaneyi döndür
		egim.y += mevcutDonusHizi * Time.deltaTime;
		rb.MoveRotation( Quaternion.Euler( egim ) );
	}
}

İtiraf ediyorum benim de umduğumdan uzun sürdü ancak güzel çalışıyor ve ben memnun kaldım.

Scriptin en başında System.Collections‘ı import ediyoruz. Scriptte coroutine‘lerden faydalanıyoruz ve coroutine’lerle çalışmak için bu kütüphane şart. Coroutine nedir diyecek olursak: çalışmasını belli/belirsiz bir süre için beklemeye alabildiğimiz özel fonksiyonlardır. Scriptin orta noktasında birkaç saniye bekleyecek misiniz? O zaman coroutine’ler işinize yarayabilir.

Unity scriptlerinizde OnEnable isimli özel bir fonksiyondan faydalanabilirsiniz. Start‘ın aksine bu fonksiyon, obje her active edildiğinde tekrar çalışır. Burada StartCoroutine fonksiyonu ile PervaneBaslat coroutine’ini çalıştırıyoruz. PervaneBaslat’ın return type‘ı IEnumerator. Ne olduğunu bilmeniz lazım değil, bu return type’ın coroutine’lerde zorunlu olduğunu bilin yeter.

PervaneBaslat içerisinde önce 1.5 saniye boyunca pervanenin belirme animasyonunun bitmesini bekliyoruz; beklemek için WaitForSeconds fonksiyonunu kullanıyoruz. Bekleme bittikten sonra pervaneye ivme veriyoruz ve uzunca bir süre pervaneyi açık tutuyoruz. Ardından pervaneyi yavaşlatıyor ve pervanenin kapanış animasyonunu oynatıyoruz (SetTrigger ile “Kapan” trigger’ını çalıştırıyoruz). Pervane çalışırken Animator‘u disable ediyoruz çünkü aksi taktirde pervanenin dönme animasyonu oyun ekranında gözükmüyor, pervane sabit duruyor (böyle bir davranışı açıkçası ben de beklemiyordum).

FixedUpdate fonksiyonunda basit anlamda pervanenin açısal hızını hesaplıyoruz ve bu açısal hızı pervaneye uyguluyoruz (MoveRotation ile). MoveRotation bir Quaternion bekliyor ve biz de elimizdeki Vector3 türündeki rotation‘ı Quaternion’a çevirmek için Quaternion.Euler fonksiyonunu kullanıyoruz.

Bu kadar açıklama yeter! Scripti size zahmet Pervane objesine atıp oyunu çalıştırın. Pervane kapandıktan sonra objeyi Inspector‘dan tekrar aktif (active) ederseniz pervanenin yeni baştan çalışmaya başladığını göreceksiniz (OnEnable sağolsun):

61_1

Artık pervane hazır olduğuna göre isterseniz onu Scene panelinden deaktif edin ki oyun başladığı gibi pervane gelmesin (tuzakları script vasıtasıyla ortaya çıkaracağız):

62

Sahaya Dinamik Engeller Koymak – Süpürge

Diğer engelimizi ise süpürge diye çağırmayı uygun gördüm. Bu engel EgikZemin‘lerin üzerinde bir uçtan bir uca giderek oradaki kişileri “süpürüyor”. Bu engel de iki küpten oluşuyor (küplerin isimleri önemli değil). Ancak küplerden önce boş bir obje (GameObject-Create Empty) oluşturup ismini “Supurge” yapın ve position(5, 0.5, 0)‘a taşıyın. Ardından iki küp objesi oluşturup onları Supurge’nin birer child objesi yaptıktan sonra aşağıdaki değerleri Transform‘larına uygulayın:

Cube: position(0, 0.5, 0), scale(0.3, 1, 0.3)

Cube: position(1.224, 1.62, 0), rotation(330, 90, 0), scale(0.3, 0.3, 3)

63

Tahmin etmişsinizdir ki ikinci küpü yerleştirmek için vertex snapping‘den faydalandım. İkinci küpün altında biraz boşluk var (topun rahatça sığabileceği kadar). Böylece bu süpürgenin altında topun geçebilmesini ama oyuncuların geçememesini sağlarak ve oyuna ekstra dinamizm katıyoruz.

Her iki küp objesine de Engel materyalini verin. Sonrasında Supurge objesine bir Rigidbody verip “Is Kinematic“i işaretleyin. İlaveten bir de “Nav Mesh Obstacle” verin:

64

Animasyon vermeye hazırız! Window-Animation ile animasyon panelini açıp Supurge‘ye “SupurgeSupur” adında yeni bir animasyon verin:

0:00 – position(5, 0.5, 0)

0:30 – position(5, 0.5, 3)

1:30 – position(5, 0.5, -3)

2:00 – position(5, 0.5, 0)

Süpürmekten kastımı anlamışsınızdır sanırım. Şimdiyse “SupurgeBeliris” adında yeni bir animasyon verin:

0:00 – position(5, -0.6, 0)

0:30 – position(5, 0.5, 0)

0:45 – position(5, 0.5, 0)

Son olarak da “SupurgeKapanis” animasyonu verelim:

0:00 – position(5, 0.5, 0)

0:30 – position(5, 0.5, 0)

1:00 – position(5, -0.6, 0)

Süpürgenin belirme ve kapanma animasyonlarının loop yapmasını istemiyoruz; bunun için de onları Project panelinden tek tek seçip “Loop Time” değerini kapatıyoruz (pervanede yaptığımız gibi).

NOT: SupurgeKapanis animasyonunda yarım saniyelik bir bekleme süresi koyduk. Bunun sebebi süpürme animasyonu ile kapanış animasyonu arasında düzgün bir yumuşak transition yapabilmek. Pervanenin kapanmadan önce tamamen durduğundan emin olduğumuz için onda herhangi transition’a ihtiyacımız yoktu ancak burada süpürgeyi kapat dediğimiz vakit süpürge ortada da olabilir uçta da. Kapanış animasyonuysa daima orta noktada oynuyor. Kapanış animasyonundaki bu yarım saniyelik bekleme anında süpürgeyi yumuşak bir şekilde ortaya getirecek ve ondan sonra kapatacağız.

Bunları da hallettikten sonra Supurge‘nin Animator component‘ine gelip “Update Mode“u “Animate Physics” olarak değiştirin. Ardından “Supurgeanimator controller‘ına çift tıklayarak Animator panelini açın. İlk önce SupurgeBeliris animasyonunun oynamasını istediğimiz için ona sağ tıklayıp “Set as Layer Default State” deyin. Bu animasyon sadece 0.75 saniye sürüyor (0:45 = saniyenin 3/4’ü). O halde kod yardımı olmadan da bu animasyondan SupurgeSupur animasyonuna geçiş yapabiliriz. SupurgeBeliris‘ten SupurgeSupur‘e transition çekin; conditions kısmını boş bırakın. Ancak “Has Exit Time“ı işaretli bırakın. Animasyonumuz 0.75 saniye sürdüğü için “Exit Time“ı 0.75 yapıp “Transition Duration“ı 1 yapın. Transition Duration dediğimiz şey iki animasyon arasında yumuşak geçiş yapılmasını sağlıyor. Ben deneye yanıla bu değerin bu animasyonlar için 1’de güzel sonuç verdiğini gördüm. Artık bu iki animasyon arasındaki geçiş (transition), belirme animasyonu bittikten sonra otomatik olarak gerçekleşecek:

65

Süpürgeyi kapatma işlemini ise kodla yapacağız ve bu yüzden bize “Kapan” adında yeni bir Trigger parametre lazım (nasıl oluşturacağınızı artık biliyorsunuz). SupurgeSupur‘den SupurgeKapanis‘a transition yapalım; koşul olarak Kapan trigger’ını koyup “Has Exit Time“ı kapatalım ve “Transition Duration“ı 0.5 yapalım (kapanış animasyonunun bekleme süresi). Sonra da “Supurge” adında yeni bir C# script oluşturalım:

using UnityEngine;
using System.Collections;

public class Supurge : MonoBehaviour 
{
	// Süpürgenin oyunda durma süresi
	public float supurgeMinimumSure = 15f;
	public float supurgeMaksimumSure = 20f;

	// OnEnable, obje her active edildiğinde çalıştırılır
	void OnEnable()
	{
		// SupurgeBaslat fonksiyonunu coroutine olarak başlat
		StopAllCoroutines();
		StartCoroutine( SupurgeBaslat() );
	}
	
	// Coroutine'lerin return type'ı IEnumerator'dur
	// ve System.Collections'ı import etmek gereklidir
	private IEnumerator SupurgeBaslat()
	{
		// Süpürgenin belirme ve süpürme animasyonları arasındaki geçiş
		// otomatik olarak gerçekleşecek. Biz sadece kapanış 
		// animasyonunu tetiklemeliyiz
		
		// Süpürge için rastgele bir çalışma süresi bul
		float supurgeSure = 0.75f + Random.Range( supurgeMinimumSure, supurgeMaksimumSure );
		
		// Süpürgenin süpürmesinin bitmesini bekle
		yield return new WaitForSeconds( supurgeSure );
		
		// Süpürgeyi kapat
		GetComponent<Animator>().SetTrigger( "Kapan" );
		
		// 1 saniye bekle (SupurgeKapanis'in süresi)
		yield return new WaitForSeconds( 1f );
		
		// Süpürge objesini inaktif yap
		gameObject.SetActive( false );
	}
}

Pervane kodunun biraz değişmiş hali sadece. “supurgeSure“yi hesaplarken 0.75 saniye, yani süpürgenin belirme animasyonunun süresini de ekliyoruz.

Scripti sahnedeki Supurge objesine verip oyunu çalıştırın:

65_1

Supurge objesini seçip Inspector‘dan inaktif yapın; dediğim gibi, engelleri script vasıtası ile ortaya çıkaracağız.

Süpürgeyi Diğer Kenarlara da Koymak

Şu anda süpürgeyi klonlayıp sahnede başka herhangi bir yere koyarsanız (nereye koyduğunuz önemli değil) süpürge animasyonunun yine de ilk süpürgenin olduğu yerde oynadığını göreceksiniz. Çünkü animasyonumuzda süpürgenin konumunu direkt olarak sabit position’lar olarak ayarlamıştık. Neyse ki animasyonları hiç ellemeden bu sorunu çözmek mümkün. Sahnede “SupurgeMerkez” adında yeni bir Empty GameObject oluşturup onu position(0,0,0)‘a taşıyın. Ardından konumu (5, 0.5, 0) olan orijinal Supurge objesini “SupurgeMerkez“in bir child objesi yapın:

66

Şimdi SupurgeMerkez objesini duplicate edip rotation(0, 270, 0) eğimini verin. Artık klon Supurge düzgün çalışıyor. Klonun Transform‘unu kontrol ederseniz position(5, 0.5, 0)‘da yer aldığını görebilirsiniz. Çünkü burada gördüğümüz koordinatlar local space‘e ait. İki adet space var: world space ve local space. Bir objeye parent atarsanız o objenin parent objeye göre göreceli konumu local space’teki konumu olur. Bizde de yeni süpürgenin SupurgeMerkez’e göre göreceli konumu (5, 0.5, 0), aslında gerçek konumu (world space) ise (0, 0.5, 5). Animasyonlar local space’te oynadığı için de yeni süpürgemiz düzgün çalışıyor.

Eğer isterseniz merkez objeyi 2 kere daha duplicate edip (0, 180, 0) ve (0, 90, 0) şeklinde eğim vererek her kenarda bir süpürge olmasını sağlayabilirsiniz.

Engelleri Rastgele Bir Şekilde Oluşturmak

Bence şu anda gameplay açısından çok kritik bir noktaya geldik. Engeller oyuna daha fazla eğlence katıyor, orada sıkıntı yok. Ancak çok fazla engel zamanla sıkıcı veya sinir bozucu olabilir. Yazacağımız kod, engelleri oyuncuyu sıkmadan rastgele bir şekilde çıkarmalı. Eğer benim yazacağım kodu beğenmezseniz kendi engel çıkarma kodunuzu yazarak oyunu daha eğlenceli kılabilirsiniz.

EngelRespawn” adında yeni bir C# script oluşturun:

using UnityEngine;
using System.Collections;

public class EngelRespawn : MonoBehaviour 
{
	// Engel objeleri
	public GameObject pervane;
	public GameObject[] supurgeler;
	
	// supurgeler array'inin uzunluğu
	private int supurgeSayisi;
	
	// Respawn süre aralığı
	public float minimumRespawnSuresi = 10f;
	public float maksimumRespawnSuresi = 25f;
	
	// Pervaneye özel cooldown süresi
	public float pervaneCooldown = 15f;
	private float pervaneKapaliSure;
	
	void Start()
	{
		supurgeSayisi = supurgeler.Length;
		
		// Pervanenin cooldown'ını oyunun başında dikkate alma
		pervaneKapaliSure = pervaneCooldown;
		
		StartCoroutine( EngelRespawner() );
	}
	
	void Update()
	{
		// Eğer pervane kapalı ise pervanenin kapalı olduğu
		// toplam süreyi artır (cooldown hesaplamak için)
		if( !pervane.activeSelf )
			pervaneKapaliSure += Time.deltaTime;
	}
	
	private IEnumerator EngelRespawner()
	{
		// Fonksiyonu sonsuza kadar çalıştır
		while( true )
		{
			// Rastgele bir respawn süresi belirle
			float respawnSuresi = Random.Range( minimumRespawnSuresi, maksimumRespawnSuresi );
			
			// Respawn süresinin dolmasını bekle
			yield return new WaitForSeconds( respawnSuresi );
			
			/***
			1 engel çıkma şansı %42
			2 engel çıkma şansı %40
			3 engel çıkma şansı %15
			Tüm engellerin çıkma şansı %3 
			***/
			// [0-1] aralığında rastgele bir float sayı üret
			float randomSayi = Random.value;
			if( randomSayi < 0.42f ) // 1 engel çıkar
			{
				RastgeleEngelCikar();
			}
			else if( randomSayi < 0.82f ) // 2 engel çıkar
			{
				RastgeleEngelCikar();
				RastgeleEngelCikar();
			}
			else if( randomSayi < 0.97f )// 3 engel çıkar
			{
				RastgeleEngelCikar();
				RastgeleEngelCikar();
				RastgeleEngelCikar();
			}
			else // Tüm engelleri çıkar
			{
				// Pervane kapalıysa pervaneyi çıkar
				if( !pervane.activeSelf )
					PervaneCikar();
				
				// Tüm süpürgeleri tek tek dolaş
				for( int i = 0; i < supurgeSayisi; i++ )
				{
					// Süpürge kapalıysa süpürgeyi çıkar
					if( !supurgeler[i].activeSelf )
						supurgeler[i].SetActive( true );
				}
			}
		}
	}
	
	// Rastgele bir engel çıkar
	private void RastgeleEngelCikar()
	{
		// Pervane çıkma şansı = 2/6 = %33
		// Süpürge çıkma şansı = 1/6 = %67
		float randomSayi = Random.value;
		if( randomSayi < 0.33f ) // Pervane çıkar
		{
			// Pervanenin kapalı olduğunu ve cooldown süresinin dolduğunu teyit et
			if( !pervane.activeSelf && pervaneKapaliSure >= pervaneCooldown )
				PervaneCikar();
			else // Pervane çıkarmak mümkün değilse süpürge çıkar
				SupurgeCikar();
		}
		else // Süpürge çıkar
		{
			SupurgeCikar();
		}
	}
	
	// Pervaneyi çıkar ve cooldown'ı resetle
	private void PervaneCikar()
	{
		pervane.SetActive( true );
		pervaneKapaliSure = 0f;
	}
	
	// Rastgele bir süpürge çıkar
	private void SupurgeCikar()
	{
		// Rastgele bir süpürge index'i bul
		int rastgeleIndex = Random.Range( 0, supurgeSayisi );
		int mevcutIndex = rastgeleIndex;
		
		do
		{
			// Süpürge kapalı ise onu çıkar
			if( !supurgeler[mevcutIndex].activeSelf )
			{
				supurgeler[mevcutIndex].SetActive( true );
				return;
			}
			
			// Bu süpürge kapalı değilse sonraki süpürgeyi çıkarmayı dene
			mevcutIndex = ( mevcutIndex + 1 ) % supurgeSayisi;
		} while( mevcutIndex != rastgeleIndex );
	}
}

Scripti olabildiğince comment’lediğim için çoğu yerin anlaşıldığını umuyorum. Tüm engelleri birer değişkende tutuyoruz. Pervane özel bir engel olduğu için onun çok sık çıkmasını engellemek adına ona özel bir cooldown değişkeni de tanımlıyoruz. Ardından EngelRespawner coroutine‘ini sonsuza dek (ya da script disable edilene dek) çalıştırıyoruz.

EngelRespawner fonksiyonu yeni engeller respawn etmek için bir süre bekliyor (WaitForSeconds), ardından Random.value ile [0,1] aralığında rastgele bir float sayı üretip bunun üzerinden olasılık testleri yapıyor. Aynı anda maksimum 3 engel çıkabiliyor ancak çok ender bir olasılıkla tüm engeller birden aynı anda çıkabiliyor da.

RastgeleEngelCikar fonksiyonu ya pervane ya da süpürge respawn ediyor ancak pervaneye biraz fazladan şans veriyor; çünkü pervane en eğlenceli engel. Eğer pervane çıkarmak mümkün değilse mecburen yine süpürge çıkarıyor.

SupurgeCikar fonksiyonu “supurgelerarray‘inden rastgele bir süpürge seçip onu çıkarmaya çalışıyor. Eğer süpürge zaten açıksa sonraki süpürgeyi çıkarmaya çalışıyor. Tüm süpürgeler birden açıksa da do-while koşulu false‘a dönüyor ve fonksiyon sonlanıyor.

Bu scripti Main Camera objesine verin ve Inspector‘dan engel değişkenlerine değerlerini verin. Burada “Supurgelerarray‘ine hızlıca nasıl değerini verebileceğinizi göstermek istiyorum. Bu yolu kullanarak array’e tüm değerlerini tek bir seferde verebiliyorsunuz. Bunun için Main Camera seçili iken sağ üstteki kilit butonuna basarak Inspector’u kilitleyin:

67

Artık başka bir objeyi seçseniz bile Inspector, Main Camera‘yı göstermeye devam edecek. Şimdi Hierarchy‘den tüm süpürge objelerini seçin ve onları tutup sürükleyerek “Supurgeler” array’inin isminin üzerine bırakın:

67_1

Oyunu çalıştırın ve sistemi test edin. Dediğim gibi, memnun kalmazsanız kendi sisteminizi de yazabilirsiniz.

NOT: Bu safhada farkettim ki bir engeli tam aktif yaptığımız anda engel, bir frame için full görünür oluyor, ardından belirme animasyonu oynuyor. Bu bir frame’lik görünür olma sıkıntısını çözmek için Scene panelinden Supurge‘lerin position Y değerini -0.6, pervanenin position Y değerini de -1.1 verebilirsiniz.

Oyunun Sonlanması

Şu anda çok önemli bir eksiğimiz var: game over! Kim 60 saniyeye ulaşırsa o kazanacak. Burada benim düşündüğüm şey oyuncunun toplam galibiyet ve mağlubiyet sayılarını cihazda depolayarak game over’da güncel veriyi ona sunmak. Bunun için de “OyunKontrol” scriptini şöyle düzenledim:

...

public class OyunKontrol : MonoBehaviour 
{
	...
	
	private bool gameOver = false;
	
	void Awake() {
		...
	}
	
	void Start() {
		...
	}
	
	void Update()
	{
		// Eğer oyun devam ediyorsa
		if( !gameOver )
		{
			// Topun sahibi kim ise onun süresini artır
			if( topSahibi == TopSahibi.Player )
			{
				...
				
				// Oyunu kazandık!
				if( playerTopSure >= 60f )
				{
					// Oyunu bitir
					gameOver = true;
					
					// Galibiyet sayısını hesapla
					int galibiyetSayisi = PlayerPrefs.GetInt( "Galibiyet", 0 );
					galibiyetSayisi = galibiyetSayisi + 1;
					
					// Galibiyet sayısını kaydet
					PlayerPrefs.SetInt( "Galibiyet", galibiyetSayisi );
					PlayerPrefs.Save();
				
					// Player'ın sayacını güncelle
					playerSayac.text = galibiyetSayisi + ". galibiyetini aldın!";
					
					// Player sayacını beyaz, düşman sayacını görünmez (clear) yap
					playerSayac.color = Color.white;
					dusmanSayac.color = Color.clear;
					
					// Düşmanın hareket etmesini engelle 
					// ve (sırf zevkine) rigidbody'sini aktifleştir
					Destroy( dusmanTransform.GetComponent<DusmanHareket>() );
					Destroy( dusmanTransform.GetComponent<NavMeshAgent>() );
					dusmanTransform.GetComponent<Animator>().SetBool( "Hareket", false );
					dusmanTransform.GetComponent<Rigidbody>().isKinematic = false;
					
					// 6 saniye sonra oyunu yeniden başlat
					CancelInvoke();
					Invoke( "YenidenBaslat", 6f );
				}
			}
			else if( topSahibi == TopSahibi.Dusman )
			{
				...
				
				// Oyunu kaybettik...
				if( dusmanTopSure >= 60f )
				{
					// Oyunu bitir
					gameOver = true;
					
					// Mağlubiyet sayısını hesapla
					int maglubiyetSayisi = PlayerPrefs.GetInt( "Maglubiyet", 0 );
					maglubiyetSayisi = maglubiyetSayisi + 1;
					
					// Mağlubiyet sayısını kaydet
					PlayerPrefs.SetInt( "Maglubiyet", maglubiyetSayisi );
					PlayerPrefs.Save();
				
					// Player'ın sayacını güncelle
					playerSayac.text = maglubiyetSayisi + " kere mağlup oldun :(";
					
					// Player sayacını beyaz, düşman sayacını görünmez (clear) yap
					playerSayac.color = Color.white;
					dusmanSayac.color = Color.clear;
					
					// Düşmanın hareket etmesini engelle 
					// ve (sırf zevkine) rigidbody'sini aktifleştir
					Destroy( dusmanTransform.GetComponent<DusmanHareket>() );
					Destroy( dusmanTransform.GetComponent<NavMeshAgent>() );
					dusmanTransform.GetComponent<Animator>().SetBool( "Hareket", false );
					dusmanTransform.GetComponent<Rigidbody>().isKinematic = false;
					
					// 6 saniye sonra oyunu yeniden başlat
					CancelInvoke();
					Invoke( "YenidenBaslat", 6f );
				}
			}
		}
	}
	
	void LateUpdate() {
		...
	}
	
	// Bir oyuncu topa değince çağrılan fonksiyon
	// topSahibi değişkeni hemen değiştirilmiyor çünkü
	// her iki oyuncunun da topa sürekli değdiği kargaşa
	// durumlarında topSahibi'nin ikide bir değer değiştirmesini
	// istemiyoruz
	public void TopaDegdim( TopSahibi degenKisi )
	{
		// Eğer oyun devam ediyorsa
		if( !gameOver )
		{
			...
		}
	}
	
	private void TopSahibiniDegistir() {
		...
	}
	
	private void YenidenBaslat()
	{
		// Oyuna restart at
		// SceneManager'lı kod Unity 5.3 öncesi sürümlerde çalışmaz
		UnityEngine.SceneManagement.SceneManager.LoadScene( UnityEngine.SceneManagement.SceneManager.GetActiveScene().name );
		
		// Unity 5.3'ten önceki bir sürümü kullanıyorsanız üstteki kodu silip
		// alttaki satırın comment'ini kaldırın:
		// Application.LoadLevel( Application.loadedLevel );
	}
}

Oyunun bitip bitmediğini depolayan gameOver adında yeni bir değişken tanımlıyorum ve Update ile TopaDegdim fonksiyonlarının içlerini “if( !gameOver )” koşulu içerisine alarak oyun bitince bu fonksiyonların çalışmasını önlüyorum.

Oyunun bitip bitmediğini, top kimdeyse onun süresinin 60’tan büyük olup olmadığını kontrol ederek öğreniyoruz (Update‘te). Eğer bizim süremiz 60’tan büyükse “PlayerPrefs.GetInt(“Galibiyet”,0)” ile cihazdan o ana kadarki galibiyet sayımızı çekiyoruz ve bu sayıyı kullanarak player’ın sayacındaki yazıyı güncelliyoruz. Ardından düşmanı işlevsel kılan tüm scriptleri yok ediyoruz ve 6 saniye sonra oyunu yeniden başlatıyoruz. Düşmanın süresi 60’tan büyükse de yine çok benzer şeyler yapıyoruz.

YenidenBaslat fonksiyonu içerisindeki LoadScene (veya LoadLevel) fonksiyonunun çalışması için ilgili sahnenin Build Settings‘teki listeye ekli olması lazım. Bunun için “File-Build Settings…” yolunu izleyip gelen penceredeki “Scenes In Build” listesine “Oyun” sahnesini sürükle-bırak yapın:

68

The End

Artık oyunumuzu tamamladık! Gerçekten uzun bir yazıydı, sizce de öyle değil mi 🙂 Şu anda oyunumuzun muhakkak eksikleri ya da hoşunuza gitmeyen yanları olabilir; ancak bence yine de oldukça güzel bir işe imza attık. Hepimizin eline sağlık o halde!

Ders boyunca oluşturduğumuz scriptlerden, modellerden vb. istediğinizce faydalanabilirsiniz.

Bunun gibi daha başka derslerde görüşmek dileğiyle!

yorum
  1. fatih dedi ki:

    İyi günler. Ben navmash sistemi kullanmaktayım. NavMeshAgent componenetinin Obstacle Avoidance>Radius kısmısı 0.5 den yukarıda yaptığım zaman karakter hareket edebiliyor fakat 0.5 den küçük yaptığımda hareket edemiyor. Baya bir kurcaladım fakat çözemedim sorunun kaynağı ne olabilir? 0.5 radius karakterim için çok büyük ve karakter bu yüzden her yere çarpıyor.

  2. osman dedi ki:

    merhabalr dersleriniz harika
    bir sorum olacaktı navmesh prefeb zeminlere ekleyebilinebir mi bake nasıl olucak bir oyunumda eklenen prefab zeminde navmesh ile hareket eden düşman yapılırmı

  3. Fatih dedi ki:

    Unity ‘de navigation bake işlemi yaptığım zaman sadece yürünemeyen objemin etrafında boşluklar oluşuyor, yürünemeyen objenin altında navigasyonun mavi bir şekilde oluştuğunu görüyorum. Nasıl tamamen o alanı yürünemez hale getirebilirim ? İyi çalışmalar

  4. Barış dedi ki:

    “Enemy” isimli objenin Nav Mesh Agent’ının tik işaretini nasıl istediğim zaman kapatıp açabilirim?

  5. Furkan Emre dedi ki:

    Mevcut index = (mevcutindex + 1) % supurgesayisi burda ne yaptığımızı anlamadım hocam yardim ederseniz sevinirim

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.