Birlikte öğreniyoruz...

Haritamızı ve düşmanlarımızın yapay zekasının kaba taslak hazırlamasını önceki bölümlerimizde tamamlamıştık. Kule kurulum menüsünü hazırlayacaktık ki araya giren yüksek lisans finalleri ve şirkette bir geçiş sürecine girmemizden dolayı, yazı biraz geç geldi. Umarım sizde bu sırada nasıl bir yapı yapabileceğimizi biraz düşünmüşsünüzdür.

Oyunumuzun ilk konusu olan “Tower Defence Oyunu Yapıyoruz” yazımızda çıkardığımız plan sadece bir kaba taslak. Biliyorsunuz ki beraber bir oyun yapmaya çalışıyoruz ve bu sırada tecrübe elde ederek, ilerde gerçekleştireceğimiz çalışmalar için kendimize bir alt yapı hazırlıyoruz. Bu sebeple bir proje planımız yok ama burada yaşadığımız zorluklardan dersler çıkaracağız.

Şimdi gelelim üçüncü bölümümüzde hangi konulara değineceğimize.

  • Zemin üzerinde mouse ile gezerken zeminin vereceği tepkileri ayarlayacağız. Örneğin bir kule seçildiğinde ya da kurulum yapıldığında gibi.
  • Kule kurulum için geçici bir menü oluşturacağız. Bu menüden tıkladığımız seçeneklere göre kule kurulumlarını gerçekleştireceğiz.
  • Düşman yapay zekasının kulelere vereceği tepkileri ayarlayacağız. Beni en çok zorlayan bu kısım oldu diyebilirim. Çünkü projemizin başında konuştuğumuz gibi yollara kuleler ya da engeller kurarak, düşmanların hedeflerine gitmelerini engellemeye çalışıyoruz. Tabi bu sırada yolu tamamen kapatmamamız gerekiyor. Ama merak etmeyin bir çözüm buldum.

Zeminin Mouse Hareketlerine Tepkilerinin Ayarlanması

Mouse ile zemin üzerinde gezerken nasıl değişiklikler olacağını belirleyeceğiz. İlk aşamadan ekleyeceklerimiz aşağıdaki gibi olacak ama yine bu konunun ilerleyen kısımlarında farklı özellikler ekleyeceğiz.

  • Mouse ile zemin üzerinde dururken nasıl tepki vereceği.
  • Mouse farklı bir zemine geçince nasıl tepki vereceği

ZeminYoneticisi” isminde bir C# script oluşturun ve “Zemin” isimli prefab objesine yani hazır şablonumuza ekliyoruz. Burada unutmamanız gereken bir kısım var. Dinamik bir harita yapısı oluşturmuştuk bu sebeple yani oyun başlamadan önce zeminleri ayarlıyoruz.

Bu yüzden zemin prefab dosyasına herhangi bir ekleme yaparsanız yeniden haritayı oluşturmanız gerekecektir. “ZeminYoneticisi” script dosyasını “Zemin” şablonuna ekledikten sonra, görseldeki gibi değerlere sahip bir harita oluşturdum.

Tower Defence Harita Ayarları
Geçici olarak kullanacağımız harita ayarları

Zemin üzerindeki değişiklikler için onMouseEnter() ve onMouseExit() fonksiyonlarını kullanacağız. Önce yazacağınız kodu aşağıya ekliyorum sonra tabi ki madde madde değerlendireceğim.

using UnityEngine;

public class ZeminYoneticisi : MonoBehaviour
{

    [SerializeField] private Material degisenMateryal, orjinalMateryal;

    private void OnMouseEnter()
    {
        if (transform.tag == "Zemin")
        {
            transform.GetComponent<Renderer>().material = degisenMateryal;
        }
    }

    private void OnMouseExit()
    {
        if (transform.tag == "Zemin")
        {
            transform.GetComponent<Renderer>().material = orjinalMateryal;
        }
    }

}

İlk olarak script dosyamıza dışarıdan obje tanımlaması yapmak için [SerializeField] private Material degisenMateryal, orjinalMateryal; yazarak “Material” türünde iki adet değişken oluşturduk. Burada yer alan “orjinalMateryal” isminde olana zemin objemizin materyal dosyasını ekleyeceğiz ve “degisenMateryal” olana ise mouse üzerine geldiğinde zeminin değişeceği materyal dosyasını ekleyeceğiz.

Zemin objesine material dosyalarının eklenmesi

OnMouseEnter() fonksiyonu, mouse zemin üzerine geldiği zaman çağrılacak olan fonksiyon oluyor. Unity Oyun motorunun varsayılan olarak gelen fonksiyonlarından biridir. Mantık olarak “Raycast” ile aynı çalışıyor gibi düşünebilirsiniz. Mouse bir zemin objesinin üzerine geldiği zaman bu fonksiyon içindekiler çalışacaktır.

OnMouseEnter” içerisine yazdığımız if bloğu yani if (transform.tag == "Zemin") ile mouse imlecinin üzerine geldiği zeminin etiketini kontrol ederek, eğer “Zemin” etiketine sahipse bloğa girmesini istemiş olduk. if bloğu içinde yazdığımız transform.GetComponent<Renderer>().material = degisenMateryal; sayesinde zemin objemizin “Renderer” bileşenine erişerek “Material” dosyasını önceden eklediğimiz “degisenMateryal” ile değiştirmiş oluyoruz.

OnMouseExit() ise tam tersi şekilde mouse zemin üzerinden ayrıldığı zaman çalışacak olan unity fonksiyonlarından biridir. “OnMouseExit” fonksiyonu içinde de “OnMouseEnter” fonksiyonunda olduğu gibi “Zemin” etiketine sahipse transform.GetComponent<Renderer>().material = degisenMateryal; ile “Material” dosyasını değiştirdik.

Burada sormanızı beklediğim sorulardan biri zaten zemin objesine eklediğimiz bir script için neden “Zemin” etiketini kontrol ediyoruz olması. Eğer bu soruyu soruyorsanız doğru yoldasınız diyebilirim. Bunun sebebi harita üzerinde yer alan engel ya da kulelerde değişiklik yapmayarak sadece zemin üzerinde değişiklik yapacak olmamız. Yani zeminin üzerinde farklı bir obje varsa bu şekilde çalışmayacak.

Diğer soru ise “Material” dosyasına erişmeye çalışırken “gameobject.taransform.GetComponent…” yerine “taransform.GetComponent…” yazarak nasıl erişebildiğimiz. Burada zaten “Zemin” objemizin içinde çalıştığımız için scriptin bağlı olduğu objenin transform ayarlarına direk “taransform.GetComponent…” yazmamız yeterli oluyor.

İşlemin sonunda aşağıdaki gibi bir sonuç elde etmiş olmanız gerekiyor.

Mouse hareketlerine göre zeminin değişmesi

Kule Kurulum Menüsünün Oluşturulması

Bu bölümde yapacağımız şey ise zeminde yer alan karelerden birine tıkladığımızda bir menü açılması sağlamak ve çıkan menüde yer alan seçeneklere tıklayarak kulenin kurulumunu sağlamak olacak. Menümüz için Unity UI yapısını kullanacağız. Unity UI içinde yer alan elemanlarının tek tek incelemesini Unity UI Dersleri konusunda incelemiştik. Eğer atlayarak bu konuya geldiyseniz bir göz atmanızı öneririm.

Menümüzün çalışma mantığı şöyle olacak. Haritada yer alan bir zemine mouse sol tuşuna tıkladığımızda bir “Canvas” açılacak ve içerisinde 3 farklı kule ve bir adet ateş etme özelliği olmayan zeminleri seçmemizi sağlayan butonlar olacak. Fakat kule kurulumu gibi işlevleri çalıştırabilmemiz için gerekli olan basit bir yapı yapacağız. İleriki süreçte bu menümüzü geliştirerek, güzelleştireceğiz.

Hierarchy” üzerine gelerek “UI/Panel” seçeneği ile sahnemize bir adet “Panel” ekliyoruz. Panelimiz otomatik olarak “Canvas” objesinin alt objesi olarak sahneye eklenecektir. Sonrasında ise “Panel” objesine tıklayarak 4 adet “UI/Button” ekliyoruz. “Button” eklemesinden sonra alt obje olarak “Text” objelerinin eklendiği görebilirsiniz. Yanlış bir ekleme yapmadıysanız hiyerarşimiz aşağıdaki gibi olacaktır.

Tower Defence oyunu için kule kurulum ekranı
Tower Defence oyunu için kule kurulum ekranı

“Canvas” objesinin boyutlarının çok büyük olduğunu fark etmişsinizdir. Şimdi “Canvas” objesini boyutlandırabilmek için “Render Mode” özelliğini “World Space” olarak değiştiriyoruz.

Bu değişiklikten sonra üst kısımda yer alan “Rect Transform” bölümünün aktif duruma geldiğini göreceksiniz. “Scale” kısmının boyutlarını “0.03” kadar düşürelim ve “Width” ve “Height” boyutlarını “0” olarak değiştirelim. Sonrasında ise “Rotation” kısmına x ekseninde “45” derecelik bir açı vererek biraz daha yatay şekilde gözükmesini sağlayacağız.

Tower Defence menüsünün canvas ayarları
Kule kurulum menüsünün canvas ayarları

Şimdi “Panel” UI bileşeninin “Inspector” kısmına girerek “Rect Transform” bölümünün ayarlarını aşağıdaki gibi değiştiriyoruz. Burada panelin boyutunu ve sahnede hangi pozisyonda olacağını ayarlamış oluyoruz.

Kule kurulum menüsünün panel ayarları
Kule kurulum menüsünün panel ayarları

Panel biraz daha görünür duruma geldi fakat tüm butonlar üst üste gelmiş gözüküyordur. Bunun için panele “Grid Layout Group” bileşenini ekleyerek, ayarlarını aşağıdaki gibi yapacağız. “Grid Layout Group” bileşeni içerisinde yer alan UI objelerini ızgara şeklinde yukarıdan aşağıya ya da yan yana dizmemizi sağlayan bir bileşen. Biz aşağıdaki ayarları yaparak butonların yan yana gelmesini ve aralarındaki mesafesi ile pozisyonunu ayarlamış olduk.

Kule kurulum menüsünün "Grid Layout Group" ayarları
Kule kurulum menüsünün “Grid Layout Group” ayarları

Şimdi Butonlar üzerinde yer alan yazıları değiştireceğiz. Bunun için “Button” altında yer alan “Text” UI objesine girerek ayarlarını aşağıdaki gibi değiştireceğiz. Yazımızın boyutu, hizası gibi birçok ayar ile butonların aşağısında gözükmesi için “Rect Transform” içerisinde değişiklikler yaptık. Buraları detaylı incelemiyorum çünkü UI derslerimizde bu seçeneklerin tek tek ne işe yaradıklarını incelemiştik.

Kule kurulum menüsünün "Button" ayarları
Kule kurulum menüsünün “Button” ayarları

Button altında yer alan yazılara rastgele kule ücretleri yazdım sizde benzer ücretler girdikten sonra aşağıdaki gibi bir sonuç elde etmeniz gerekiyor.

Kule kurulum menüsünün ön görünüşü
Kule kurulum menüsünün ön görünüşü

Şimdi yapacağımız bir kaç işlem kaldı. İlk işimiz butonların standart görünümü yerine bir görsel eklememiz. Burada ekleyeceğiniz görselin “png” uzantılı olması gerekiyor ki arka planında her hangi bir renk olmasın. Sizler için toplam 4 adet kule resmi bularak aşağıya bağlantısını ekledim. Buradan indirdiklerinizi “Assets” klasörünüz içine “Icons” isminde bir klasör oluşturarak içerisine koyabilirsiniz.

Kule Kurulum Menüsü İkonları

Not: Bu ikonları elde etmek için asset store üzerinden ücretsiz indirerek, dersimizin ilerleyen süreçlerinde kullanacağımız kulelerin görsellerinden elde ettim. Kulelerin ekran görüntülerini alarak arka planlarını sildim ve png dosyası haline getirdim. Arka plan silme işlemleri “Photoshop” ile yaptım ama https://www.remove.bg/ adresini de kullanabilirsiniz.

“Icons” klasörümüzün içerisine kopyalama işlemini gerçekleştirdiysek, resimleri kullanabilmemiz için bir işlem daha yapmamız gerekiyor. Bunun için resimlerimizin hepsinin aşağıdaki gibi “Sprite(2D and UI)” olacak türlerini değiştireceğiz. Tüm görselleri aynı işlemi yapmak için hepsini seçtikten sonra ayarları yapabilirsiniz. Değişiklik sonrası “Apply” butonuna basmayı unutmayın.

Buton görsellerinin hazırlanması
Buton görsellerinin hazırlanması

Şimdi butonlarımıza geri dönerek “Image” bileşenin altında yer alan “Source Image” kısmından eklediğimiz görselleri seçeceğiz. Tabi öncesinde butonlara sırasıyla aşağıdaki gibi isimler vererek seçeceğiniz görselleri isimle uyumlu olanları seçebilirsiniz.

Buton görsellerinin eklenmesi
Buton görsellerinin eklenmesi

Buton görsellerini ekledikten sonra yukarıda gösterdiğim gibi gözükecektir. Şuan menümüz hazır ama bir özellik daha ekleyeceğiz. Oyunu başlattıktan sonra menü üzerindeki resimlerde gezerken renginin değişerek hangisine tıkladığımızı görsek daha iyi olacak. Bunun için butonlarımızın “Highlighted Color” ayarını aşağıdaki gibi değiştiriyoruz. Böylece buton görsellerinin üzerine gelince rengi solacaktır. Tabi tüm butonlar için bu işlemi yapacaksınız.

unity tower defence button highlighted ayari
Button hightlighted color ayarı

Evet sahneye bir menü ekledik ama bu menü sürekli ekranda gözüküyor ve sabit bir pozisyonda duruyor. Ama bizim amacımız sahnede yer alan zemin öğelerinden birine tıkladığımızda görünür duruma gelmesi ve zeminin hemen üstünde pozisyonunu ayarlaması.

O zaman başlayalım...

Biraz planlı gitmemiz gerekiyor çünkü ileride kodlarımız çok karışacak ve yönetmesi zor olacaktır. Bu sebeple tüm kurulum işlemlerimizi yönetebilmek için “KurulumYoneticisi” ve menülerimiz içinse “MenuYoneticisi” isminde C# Script dosyaları oluşturalım ve sahnemizde yer alan “OyunYoneticisi” isimli objemize ekleyelim.

Bu iki adet script dosyasının birbirlerine erişerek parametre göndermesi gereken ya da alması gereken durumlar olacak. Unity’de bir scripte farklı bir script üzerinden erişebilmek için üç farklı yöntem bulunuyor. Bizim burada kullanacağımız en hızlı yöntem olan Instance kullanımı olacak. Bu yöntemle “Static” bir değişken oluşturarak bu değişkene “Awake” fonksiyonunda bulunduğu scripti atamak ve dışarıdan tüm scripte erişilmesini sağlamak olacak. Diğer script erişim yöntemleri ve bu yöntemin detaylarıyla ilgili farklı bir içerik hazırlamayı kendime not ettim.

KurulumYoneticisi” isimli script dosyamızı açarak içerisine aşağıdaki kodları ekliyoruz.

using UnityEngine;

public class KurulumYoneticisi : MonoBehaviour
{
    public static KurulumYoneticisi instance;

    private void Awake()
    {
        if (instance != null) return;
        instance = this;
    }
}

MenuYoneticisi” isimli script dosyamızı da açarak içerisine aşağıdaki kodları ekliyoruz.

using UnityEngine;

public class MenuYoneticisi : MonoBehaviour
{
    public static MenuYoneticisi instance;

    private void Awake()
    {
        if (instance != null) return;
        instance = this;
    } 
}

public static KurulumYoneticisi instance; ya da public static MenuYoneticisi instance; yazarak “Static” ve “Public” olan bir değişken oluşturduk. Burada geçen kalıplaşmış değişken ismi “instance” olduğu için aynı şekilde kullandık.

Sonrasında ise “Awake” Unity fonksiyonu içerisinde instance = this; yazarak “instance” değişkenine “this” yani içinde bulunduğu objeyi(script dosyasını) atamış olduk. Üstünde yazan if (instance != null) return; ise değişken zaten boş değilse geri döndürmek için kullandığımız bir kontrol yapısı.

Not: Unity fonksiyonları arasında yer alan “Awake” fonksiyonunu ve diğer fonksiyonları “Unity Fonksiyonları” dersinde incelemiştik. “Start” fonksiyonu yerine “Awake” fonksiyonu içinde kullanmamızın sebebi ise “Awake” fonksiyonunun “Start” fonksiyonundan daha önce çalışıyor olması.

“Instance” yapısını oluşturduktan sonra script dosyalarının birbirlerine erişebilmesi için diğer scipten obje oluşturarak instance değişkenini atamalıyız. Bunun için “KurulumYoneticisi” ve “MenuYoneticisi “içerisine aşağıdaki kısımları ekliyoruz.

Kurulum Yöneticisi

using UnityEngine;

public class KurulumYoneticisi : MonoBehaviour
{
    public static KurulumYoneticisi instance;
    private MenuYoneticisi menuYoneticisi;

    private void Awake()
    {
        if (instance != null) return;
        instance = this;
    }

    private void Start()
    {
        menuYoneticisi = MenuYoneticisi.instance;
    }

}

Menü Yöneticisi

using UnityEngine;

public class MenuYoneticisi : MonoBehaviour
{
    public static MenuYoneticisi instance;
    private KurulumYoneticisi kurulumYoneticisi;

    private void Awake()
    {
        if (instance != null) return;
        instance = this;
    }

    private void Start()
    {
        kurulumYoneticisi = KurulumYoneticisi.instance;
    }
}

Burada eklediğimiz private KurulumYoneticisi kurulumYoneticisi; ve private MenuYoneticisi menuYoneticisi; ile diğer scriptten bir obje oluşturuyoruz ve “Start” fonksiyonu içerisinde kurulumYoneticisi = KurulumYoneticisi.instance; ve menuYoneticisi = MenuYoneticisi.instance; yazarak oluşturduğumuz objeye instance objesini atıyoruz.

Scriptlere eklediğimiz bu instance yöntemi ile her iki script birbirleri içeresinde olan değişkenken ve fonksiyonlara erişebilecek. Örneğin “MenuYoneticisi” içerisinde yer alan “Hesapla()” isminde bir fonksiyonumuz varsa “KurulumYoneticisi” içerisinden menuYoneticisi.Hesapla() yazarak erişebileceğiz.

Daha fazla kafamızı karıştırmadan devam edelim. “KurulumYoneticisi” scriptine bazı kodlar ekleyerek aşağıdaki duruma getirdik. Önce ekleyin sonra beraber inceleyelim.

using UnityEngine;
using UnityEngine.EventSystems;

public class KurulumYoneticisi : MonoBehaviour
{
    public static KurulumYoneticisi instance;

    private GameObject menu;
    [SerializeField] private Camera kamera;

    private MenuYoneticisi menuYoneticisi;

    private void Awake()
    {
        if (instance != null) return;
        instance = this;
    }

    private void Start()
    {
        menu = GameObject.FindWithTag("KuleKurulumMenusu");

        menuYoneticisi = MenuYoneticisi.instance;   
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0) && !EventSystem.current.IsPointerOverGameObject())
        {
            ZeminKontrol();
        }
    }

    public void ZeminKontrol()
    {
        RaycastHit hedef;
        Ray isin = kamera.ScreenPointToRay(Input.mousePosition);

        if (Physics.Raycast(isin, out hedef, Mathf.Infinity))
        {

            if (hedef.transform.CompareTag("Zemin"))
            {
                menuYoneticisi.KurulumMenusuGoster(hedef.transform.position);
            }
            else if (hedef.transform.CompareTag("Kuleler"))
            {
                Debug.Log("Kule Secildiği Zaman Ne Yapılacağını Gireceğiniz Kısım");
            }
        }
    }
}

“public void ZeminKontrol()” kısmında bir fonksiyon oluşturduk ve bu fonksiyon içerisinde “Raycast” sistemini kullandık. Umarım nedir bu “Raycast” diye sormuyorsunuzdur diye ümit ediyorum. Eğer soruyorsanız hemen detaylı bir şekilde raycast sistemini incelediğimiz “Unity Raycast Dersleri” konusuna dönmenizi öneririm.

Yine de yapacağımızı mantığı anlatalım. Scripte [SerializeField] private Camera kamera; eklediğimiz kod ile dışarıdan obje olarak aktaracağımız kameradan mouse imlecinin olduğu kısma bir ışın göndereceğiz. Böylece gönderdiğimiz ışının hangi objeye isabet ettiğini “Raycast” sistemi sayeside öğreneceğiz.

Bunu yapmamızın sebebi ise mouse ile hangi objeye tıkladığımızı bulabilmek. Böylece boş bir zemine tıkladığımızda açılacak olan menü ile bir kule üzerine tıkladığımızda açılacak olan menüyü ayırt edebileceğiz. Bu ayırt etme işlemini ise if..else if bloğu içinde yazdığımız hedef.transform.CompareTag("Zemin") ile yapıyoruz. Yani “Raycast” sistemi ile ışının temas ettiği objenin etiketi “Zemin” ise bu bloktaki kodları çalıştır ve eğer “Kuleler” ise bu kısımdakini çalıştır gibi.

“ZeminKontrol()” fonksiyonunu sürekli çalıştırmamız lazım fakat sürekli olarak sahnedeki her objeyi de kontrol etmesini istemiyoruz. Bu sebeple “Update()” fonksiyonu içerisine bir if bloğu ekleyerek içerisine Input.GetMouseButtonDown(0) && !EventSystem.current.IsPointerOverGameObject() yazıyoruz.

if bloğu içinde yer alan “ZeminKontrol()” fonksiyonumuz Input.GetMouseButtonDown(0) değeri true döndüğü zaman yani mouse sol tuşu ile bir objeye tıklandığında ve bir UI ekranı içinde bir alana tıklanmıyorsa. Yani burada kullanılan !EventSystem.current.IsPointerOverGameObject() bize bir UI ile gösterdiğimiz menüye mi yoksa menü haricinde UI objesi olmayan bir alana mı tıkladığımızı döndürmektedir. Eğer bir UI elemanına tıklarsanız true dönecektir. Biz menüye değil zemine tıklandığını tespit etmek için false dönmesine ihtiyacımız var o yüzden “!” kullanarak false geldiyse çalışmasını istedik.

Input sistemini dersini atladıysanız Unity Input Dersleri konumuza göz atabilirsiniz.

Peki mouse ile sahnede yer alan bir “zemin” etiketine sahip objeye yani zemine tıkladık ve kodumuzda yer alan if bloğu içindeki menuYoneticisi.KurulumMenusuGoster(hedef.transform.position); kısmı çalıştı. Burada demiş ki “MenuYoneticisi” scripti içerisine “KurulumMenusunuGoster” diye bir fonksiyon var onu çağır ve içerisine hedef.transform.position gönder yani raycast’in hedeflediği objenin pozisyon bilgilerini.

O zaman bize “MenuYoneticisi” scripti içerisinde “KurulumMenusunuGoster” fonksiyonu gerekiyor. Hadi ekleyelim…

using UnityEngine;

public class MenuYoneticisi : MonoBehaviour
{
    public static MenuYoneticisi instance;

    private KurulumYoneticisi kurulumYoneticisi;

    [SerializeField] private GameObject ilkKurulumMenusuSablon;

    private void Awake()
    {
        if (instance != null) return;
        instance = this;
    }

    private void Start()
    {
        kurulumYoneticisi = KurulumYoneticisi.instance;
    }

    public void KurulumMenusuGoster(Vector3 _pozisyon)
    {
        ilkKurulumMenusuSablon.transform.position = _pozisyon;
        ilkKurulumMenusuSablon.SetActive(true);
    }
}

[SerializeField] private GameObject ilkKurulumMenusuSablon; yazarak dışarıdan bir obje ataması yapacağız. Atama yapacağımız obje ise oyuna eklediğimiz kule kurulum menümüz olacak. Menü atamasını yaptıktan sonra çağıracağımız “KurulumMenusuGoster” fonksiyonuna bakarsanız ilkKurulumMenusuSablon.transform.position = _pozisyon; yazarak “Raycast” ile gönderdiğimiz pozisyon bilgisini menümüze aktarıyoruz. Yani mouse ile tıkladığımız zemini bu fonksiyona gönderdikten sonra fonksiyon aracılığıyla menünün pozisyonunu tıklanan zeminle aynı yapmış olduk.

Diğer komutumuz ise ilkKurulumMenusuSablon.SetActive(true); yani menünün görünümünü aktif yapıyoruz. Tabi önce menü görünümünü pasif yapmalıyız. Bunu yapma sebebimiz ise oyun başladığında menünün gözükmemesi lazım, biz sahnede bir zemine tıkladığımızda gözükmesi lazım. O yüzden önce pasif yapıyoruz ve biz tıklayınca menü görünür oluyor ve pozisyonu değişiyor.

Kule savunma oyunumuzda birden fazla menü yapısı olacağı için şimdiden sahnemizi düzene sokalım. Önce bir “Create Empty” diyerek boş bir obje oluşturarak ismini “IlkKurulumMenusu” yapalım ve oluşturduğumuz menüyü içerisine aktaralım. Sonrasında ise tekrardan boş bir obje oluşturarak ismini “Menuler” yapalım ve yine içine atalım. Böylece bir hiyerarşi oluşturmuş oluruz.

Kurulum menüsü hiyerarşisi
Kurulum menüsü hiyerarşisi

Menümüzü ve kameramızı script dosyalarımıza eklediğimiz zaman çalışmaya hazır bir duruma gelecektir.

unity tower defence scripte dosyalari atama

Hiyerarşinin en altında bir adet “Image” eklenmiş olduğunu görebilirsiniz. Bunu sonradan ekledim sizinde eklemeniz için “UI/Image” ile sahneye bir adet “Image” ekleyin. Eklene “Image” UI elemanını “Canvas” içerisine koyduktan sonra ayarlarını aşağıdaki gibi yapın.

Menuye ok simgesi ekleme
Menuye ok simgesi ekleme

Seçilen zemin ve menü arasına bir ok simgesi koyarak, açılan menünün hangi zemin olduğunu göstermiş olacağız. Ok simgesini indirmek için aşağıdaki bağlantıyı kullanabilirsiniz. İndirme işleminden sonra “Icons” klasörüne kopyalayın ve kule görsellerinde olduğu gibi “Sprite(2D and UI)” türüne çevirin.

Ok ikonu

Bu bölümün sonunda aşağıdaki gibi zeminde bir alana tıklandığı zaman menünün otomatik gösterilmesini sağlayabilirsiniz.

Kurulum menüsünün tamamlanmış taslağı

Kulelerin Kurulumu Yapmak İçin Gerekenler

Menümüze eklediğiniz kule simgelerine tıklandığı zaman seçilen zemin üzerine kurulum işlemini gerçekleştirmemiz gerekiyor. Menü içerisine eklediğimiz butonlarda aslında bizim için bu işi yapacaklar. Oyuncu kule resmi olan butonlara tıkladığı zaman biz arka planda bir fonksiyon çağıracağız ve seçili kule için kurulum işlemlerini yaptıracağız.

İlk olarak daha önceden oluşturduğumuz “MenuYonetici” isimli script dosyamıza aşağıda işaretlediğim kodları ekleyeceğiz.

using UnityEngine;

public class MenuYoneticisi : MonoBehaviour
{
    public static MenuYoneticisi instance;

    private KurulumYoneticisi kurulumYoneticisi;

    [SerializeField] private GameObject ilkKurulumMenusuSablon;
    [SerializeField] private  GameObject engel,birinciKule, ikinciKule, ucuncuKule;
    private GameObject kurulanKule;
    private Vector3 pozisyon;

    private void Awake()
    {
        if (instance != null) return;
        instance = this;
    }

    private void Start()
    {
        kurulumYoneticisi = KurulumYoneticisi.instance;
    }

    public void birinciKuleSecimi()
    {
        kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(birinciKule, pozisyon);
        if (kurulanKule != null) KurulumMenusuGizle();
    }

    public void IkinciKuleSecimi()
    {
        kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(ikinciKule, pozisyon);
        if (kurulanKule != null) KurulumMenusuGizle();
    }

    public void UcuncuKuleSecimi()
    {
        kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(ucuncuKule, pozisyon);
        if (kurulanKule != null) KurulumMenusuGizle();
    }

    public void EngelSecimi()
    {

        kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(engel, pozisyon);
        if (kurulanKule != null) KurulumMenusuGizle();
    }

    public void KurulumMenusuGoster(Vector3 _pozisyon)
    {
        ilkKurulumMenusuSablon.transform.position = _pozisyon;
        ilkKurulumMenusuSablon.SetActive(true);
        pozisyon = _pozisyon;
    }

    public void KurulumMenusuGizle()
    {
        ilkKurulumMenusuSablon.SetActive(false);
    }
}

İlk başta bize lazım olacak olacak olan değişkenlerimizi oluşturuyoruz. [SerializeField] private  GameObject engel,birinciKule, ikinciKule, ucuncuKule; yazarak dışarıdan script dosyasına ekleyeceğimiz kule şablonları için GameObject türünde değişkenler oluşturduk. Ek olarak fonksiyonlar için kurulum yapılıp yapılamadığını test etmek içinde private GameObject kurulanKule; yazarak yine GameObject türünde bir değişken oluşturduk. Ayrıca daha önceden eklediğimiz “KurulumMenusunuGoster” fonksiyonu aracılığıyla bize gelen pozisyon bilgisini atamak için private Vector3 pozisyon; yazarak Vector3 türünde bir değişken oluşturduk.

Kodumuzu incelediğimizde her bir kule yapısı ve zemin için ayrı ayrı fonksiyonlar olduğunu göreceksiniz. Örneğin birinciKuleSecimi() isimli fonksiyonumuz açılan menüden birinci kuleyi seçtiğinde çalışacak olan fonksiyon ya da EngelSecimi() fonksiyonu ise menüdeki engeli seçtiğimiz zaman çalışacak olan fonksiyon oluyor.

Her fonksiyonun içerisinde kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(birinciKule, pozisyon); yazarak daha sonradan “KurulumYoneticisi” script dosyasına ekleyeceğimiz fonksiyonu çağırdık. Parametre olarak ise “Raycast” ile aldığımız pozisyon bilgisini ve hangi kulenin kurulacağı bilgisini gönderdik.

“KurulumYoneticisi” scriptine ekleyeceğimiz fonksiyonu içinde “Instantiate” yöntemini kullanarak sahnede obje oluşturacağız ve bize oluşturduğu objenin dönmesini sağlayacağız. Eğer kule kurulumu yapılamazsa da null değerini dönecek. Kodumuzda yer alan if (kurulanKule != null) KurulumMenusuGizle(); bunu kontrol etmemizi sağlıyor. Burada şöyle düşünebilirsiniz. Kule menüsünü açtınız ve kulelerden birini seçtiğiniz zemine kurduğunuz. Bu işlemden sonra tabi ki menünün kapanması gerekiyor ki yeni zemine tıklandığında yeniden açabilelim. Bizde eğer kule kurulumu yapıldıysa “KurulumMenusuGizle()” fonksiyonunu çalıştır demiş oluyoruz.

KurulumMenusuGizle()” fonksiyonunun içindeyse menümüzün görünürlüğünü kapatması için ilkKurulumMenusuSablon.SetActive(false); komutunu ekledik.

Şimdi “MenuYonetici” kısmında yapacağımız işlemleri bitirdik. Sıra geldi “KurulumYoneticisi” C# script dosyana eklememiz gereken “SecilenKuleyiKur” fonksiyonuna. Bunun için “KurulumYoneticisi” script dosyasının istediğiniz kısmına aşağıdaki fonksiyonu ekleyebilirsiniz.

public GameObject SecilenKuleyiKur(GameObject _kuleSablonu, Vector3 _pozisyon)
{
    GameObject eklenenKule = Instantiate(_kuleSablonu, _pozisyon, Quaternion.identity);

    if (eklenenKule != null)
    {
        return eklenenKule;
    } else return null;
}

Fonksiyonumuz oldukça basit. Gelen kule şablonunun bilgisi ve pozisyon bilgilerini al ve Instantiate(_kuleSablonu, _pozisyon, Quaternion.identity); ile sahnede bir obje oluştur. Sonrasında ise if..else bloğu ile fonksiyonu çağıran tarafa kurulan kulenin bilgisini ya da başarısız olması durumda “null” bilgisini gönder.

Her iki C# script dosyasına bu eklemeleri yaptığınızda kod kısmımız tamam demek oluyor ama eğer deneme yaptıysanız menü öğelerine tıkladığınızda henüz bir işlem yapmadığını görmüşsünüzdür. Bunun için butonlara sana tıklandığında “MenuYoneticisi” içindeki bu fonksiyonu ya da şu fonksiyonu çalıştır dememiz gerekiyor.

Bu işlem için aşağıda gösterildiği gibi her bir menü elemanı için fonksiyon eklememiz gerekiyor. “+” simgesine tıkladıktan sonra “Object” seçme alanından hangi objede yer alan fonksiyonu seçeceğimizi belirliyoruz. Burada sahnede yer alan “OyunYoneticisi” objesini seçiyoruz. Bunun sebebi fonksiyonların yer aldığı “MenuYonetici” script dosyası “OyunYoneticisi” objesinin içinde ekli olarak bulunması. Son olarak ise “Runtime Only” yanında yer alan kısımdan hangi fonksiyonun çalışacağını seçiyoruz.

Fonksiyonları butonlara ekleme
Fonksiyonları butonlara ekleme

O zaman sırada kule kurulumları için kullanacağımız objeleri ayarlamaya geldi. Bunun için “Asset Store” den ücretsiz olarak indirebileceğiniz “Fatty Poly Turret Part 2 Free” dosyasını kullandım. İndirme ve projemize import işleminden sonra “Assets/FattyPolyTurretPart2Free/Prefabs” klasörü içinde yer alan “FattyCatapultG02”, “FattyMissileG02” ve “FattyMissileG03” hazır şablonlarını kendi “Assets/Prefabs” klasörümüze kopyalıyoruz.

Menüye eklediğimiz görsellerde aslında buradaki kulelerin küçük simgeleri oluyor. Görseldeki gibi şablonların isimlerini değiştirebilirsiniz.

Kule şablon dosyaları
Kule şablon dosyaları

Ayrıca kulelerin sahneye eklendiğinde düşmanlarının yolunu kapatabilmesini sağlamamız gerekiyor. Bunun için tüm kule ve zemin şablonlarına “Nav Mesh Obstacle” bileşeninin eklenmesi. Bu şekilde düşmanın önüne bir kule yerleştirildiğinde yolunu değiştirmek zorunda kalacaktır. Ayrıca ilerde kullanabilmemiz içinde tüm kulelere “Kuleler” isminde tag yani etiket ekledik.

unity tower defence nav mesh obstacle
Nav Mesh Obstacle Ayarları

Not: Kule renkleriniz benimkinden farklı gözüküyor ve aynı gözükmesini istiyorsanız “Assets/FattyPolyTurretPart2Free/Materials/” içinde yer alan “Palette256” materyal dosyasına tıklayarak inspector kısmındaki ayalarını aşağıdaki gibi yapın.

Tilling: X- 0,67 | Y – 0,9
Offset: X – 0 | Y- 0,15

Bu işlemi menüde yer alan tüm seçenekler için yaptığımızda aşağıdaki gibi bir sonuç elde etmeniz lazım.

Kule kurulum sisteminin tamamlanmış hali

Kule Seçimlerine Göre Zeminde Gerçekleşecek Olan Değişiklikler

Kule kurulum işlemlerimizi yaptık ama bu işlemler sırasında bazı eksiklikler olduğunu fark ettim. Örneğin zemini seçtikten sonra menüde kule seçmeye çalışırken hangi zemini seçtiğimiz tam olarak belli olmuyor. Bir de kule kurulumu tamamlandıktan sonra zeminin rengini değiştirerek daha oynanabilir hale getirebiliriz.

“ZeminYoneticisi” isimli C# script dosyamıza aşağıdaki üç fonksiyonu ekleyelim.

public void ZeminDegistir(RaycastHit _hedef)
{   
   _hedef.transform.GetComponent<Renderer>().material.color = Color.red;
   _hedef.transform.tag = "SecilmisZemin";
}
public void SeciliZeminiSil()
    {
        GameObject secilmisZeminiSil = GameObject.FindWithTag("SecilmisZemin");

        if (secilmisZeminiSil != null)
        {
            secilmisZeminiSil.transform.tag = "Zemin";
            secilmisZeminiSil.GetComponent<Renderer>().material = orjinalMateryal;
        }
    }
public void ZeminiSeciliYap()
{
    GameObject secilenZemin = GameObject.FindWithTag("SecilmisZemin");

    if (secilenZemin != null)
    {
        secilenZemin.transform.tag = "DoluZemin";
        secilenZemin.transform.GetComponent<Renderer>().material.color = Color.gray;
    }
}

ZeminDegistir() fonksiyonunu “KurulumYoneticisi” script dosyasından çağıracağız. Böylece “KurulumYoneticisi” içerisinde yer alan “Raycast” ile elde ettiğimiz hedef bilgisini gönderebileceğiz.

Burada şöyle bir taktik yaptık. “OnMouseEnter” ve “OnMouseExit” fonksiyonlarını kullanırken etiketi “Zemin” olanlar için çalışmasını istemiştik. Bu yüzden “SecilmisZemin” isminde yeni bir etiket ekledik ve fonksiyonun içerisinde _hedef.transform.tag = "SecilmisZemin"; yazarak seçilen zeminin etiket bilgisini değiştirmiş olduk. hedef.transform.GetComponent<Renderer>().material.color = Color.red; yazarak ise zeminin materyaline erişerek “Color” sınıfından kırmızı rengi atadık.

SeciliZeminiSil() fonksiyonu da aynı şekilde “KurulumYoneticisi” script dosyasından çağıracağız. “ZeminDegistir” fonksiyonu ile kırmızı hale getirdiğimiz zemini eğer herhangi bir kurulum yapılmadan kapatılma durumu olursa eski haline getirmemiz gerekiyor.

GameObject secilmisZeminiSil = GameObject.FindWithTag("SecilmisZemin"); yazarak “SecilmisZemin” etiketine sahip olan zemini bularak “GameObject” objesine atıyoruz. Sonrasında ise secilmisZeminiSil != null ile gerçekten bu etikete sahip bir zemin olup olmadığını kontrol ediyoruz. Eğer sahnede “SecilmisZemin” etiketine sahip bir obje varsa secilmisZeminiSil.transform.tag = "Zemin"; yazarak objenin etiketini değiştirerek secilmisZeminiSil.GetComponent().material = orjinalMateryal; ile de eski haline getiriyoruz.

ZeminiSeciliYap() fonksiyonuyla ise bir zemin seçildikten sonra açılan menüden bir kule kurulumu seçilirse kulenin kurulduğu alanın rengini değiştireceğiz. Diğer fonksiyonlarda olduğu gibi GameObject secilenZemin = GameObject.FindWithTag("SecilmisZemin"); yazarak önce zemini buluyoruz. Sonrasında ise “DoluZemin” isminde yeni bir etiket ekleyerek secilenZemin.transform.tag = "DoluZemin"; kod satırıyla zeminin etiketini değiştiriyoruz. Son olarak ise yine zeminin materyal rengini değiştiriyoruz sadece bu sefer secilenZemin.transform.GetComponent().material.color = Color.gray; yazarak gri rengine çeviriyoruz.

ZeminDegistir” ve “SeciliZeminiSil” fonksiyonlarını çağıracağımız kısım “KurulumYoneticisi” script dosyası içinde yer alan ve daha önceden eklediğimiz “ZeminKontrol” fonksiyonu olacak. Aşağıda işaretlenmiş olan kısımları fonksiyonun içerisine ekleyebilirsiniz.

public void ZeminKontrol()
{
    RaycastHit hedef;
    Ray isin = kamera.ScreenPointToRay(Input.mousePosition);

    if (Physics.Raycast(isin, out hedef, Mathf.Infinity))
    {
        zemin.SeciliZeminiSil();

        if (hedef.transform.CompareTag("Zemin"))
        {
            if (EventSystem.current.IsPointerOverGameObject()) return;

            zemin.ZeminDegistir(hedef);
            menuYoneticisi.KurulumMenusuGoster(hedef.transform.position);
        }
        else if (hedef.transform.CompareTag("Kuleler"))
        {
            Debug.Log("Kule Secildiği Zaman Ne Yapılacağını Giriceğiniz Kısım");
        }
    }
}

ZeminiSeciliYap” fonksiyonunu ekleyeceğimiz kısım ise “MenuYoneticisi” içinde her kule ve zemin için eklediğimiz fonksiyonların içleri olacak. Aşağıda işaretlenmiş alanlardaki gibi her fonksiyonun ilk kısmına ekleyebilirsiniz.

public void birinciKuleSecimi()
{
    zemin.ZeminiSeciliYap();

    kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(birinciKule, pozisyon);
    if (kurulanKule != null) KurulumMenusuGizle();
}

public void IkinciKuleSecimi()
{
    zemin.ZeminiSeciliYap();

    kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(ikinciKule, pozisyon);
    if (kurulanKule != null) KurulumMenusuGizle();
}

public void UcuncuKuleSecimi()
{
    zemin.ZeminiSeciliYap();

    kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(ucuncuKule, pozisyon);
    if (kurulanKule != null) KurulumMenusuGizle();
}

public void EngelSecimi()
{
    zemin.ZeminiSeciliYap();

    kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(engel, pozisyon);
    if (kurulanKule != null) KurulumMenusuGizle();
}

Tüm işlemlerden sonra zemindeki renk değişikliklerinizin videodaki gibi olması gerekiyor.

Zemin değişikliklerinin tamamlanmış hali

Engel ve Kulelerin Yolu Kapatmasını Engellemek

Düşman karakterlerinin bitiş noktalarına giderken önlerine kule ve engel koyarak yolu uzatmasını sağlayabiliyoruz. Fakat burada kontrol etmemiz gereken kritik bir konu bulunuyor. Eğer düşman karakterlerinin yolunu kulelerle tamamen kapatırsak hareket etmeyi durduracakları için kolay bir hedef olacaktır. Ayrıca bu durum oynanabilirliği de oldukça kötü etkileyen bir özellik olur.

İşin aslı bu durum için başlarda çözüm bulmakta oldukça zorlandığımı söyleyebilirim. Düşman karakterlerimiz navigasyon sistemine göre hareket ettikleri için hedef olarak belirlenen yere hala erişimlerinin olup olmadığını “pathStatus” ile görebiliyoruz. Fakat buradaki sıkıntılı kısım yolu kapattıktan sonra bunu öğrenebilmemiz.

Bu yüzden çözüm olarak kendimce bir çözüm ürettim. Kırmızı ve mavi düşman karakterleri için ayrı ayrı bir şekilde “Coroutine” fonksiyonunu kullanarak “SetDestination” durumlarını kontrol ettim ve eğer kurulum sonrasında yol kapanırsa aynı anda “Destroy” işlemini yapıyorum. İleride daha stabil bir çözüm bulursak bu kısmı değiştirebiliriz.

İşlemlerin nasıl gerçekleştiğini incelemeden önce Coroutine Foknsiyonu, Destroy Fonksiyonu ve Unity AI Sistemi derslerine göz atmanızı öneririm.

İlk olarak projemize daha önceden eklediğimiz “DusmanDalgalariYonetici” C# script dosyamızın en sonuna aşağıdaki iki adet fonksiyonu ekliyoruz.

public bool KirmiziHedefErisilebilirMi()
{
    yapayZekaKirmizi.SetDestination(kirmiziBitisNoktasi.transform.position);

    if (yapayZekaKirmizi.pathStatus == NavMeshPathStatus.PathPartial) return false;
    else return true;
}

public bool MaviHedefErisilebilirMi()
{
    yapayZekaMavi.SetDestination(maviBitisNoktasi.transform.position);

    if (yapayZekaMavi.pathStatus == NavMeshPathStatus.PathPartial) return false;
    else return true;
}

Her iki fonksiyon çağrıldığı zaman “Boolean” bir değer yani True ya da False bir sonuç döndürecektir. Fonksiyon içinde düşman yapay zekalarını tekrar hedefe yönlendirmek için yapayZekaKirmizi.SetDestination(kirmiziBitisNoktasi.transform.position); ve yapayZekaMavi.SetDestination(maviBitisNoktasi.transform.position); yazdık.

Tekrar yönlendirme sonrasında ise yapayZekaMavi.pathStatus ile hedefin hala erişilebilir olup olmadığını kontrol ettiriyoruz. Eğer hedefe erişim yoksa yani düşmanların yolu kapalıysa “pathStatus” bize “PathPartial” sonucunu dönecektir. if..else bloğu içinde kullandığımız NavMeshPathStatus.PathPartial eşit olması durumunda bize dönen sonuçtur false ve eğer yol açıksa true değerlerini dönecektir.

KirmiziHedefErisilebilirMi” ve “MaviHedefErisilebilirMi” fonksiyonlarını çağıracağımız yer ise “MenuYoneticisi” isimli script olacaktır. Bunun için scrip dosyasının içerisine aşağıdaki gibi yeni bir fonksiyon ekliyoruz.

public void YolDurumunuKontrolEt()
{
    StartCoroutine(KirmiziYoluKontrolEt());
    StartCoroutine(MaviYoluKontrolEt());

    IEnumerator KirmiziYoluKontrolEt()
    {
        yield return new WaitUntil(() => dusmanDalgalariYoneticisi.KirmiziHedefErisilebilirMi() == false);
        Destroy(kurulanKule);
    }

    IEnumerator MaviYoluKontrolEt()
    {
        yield return new WaitUntil(() => dusmanDalgalariYoneticisi.MaviHedefErisilebilirMi() == false);
        Destroy(kurulanKule);
    }
}

Fonksiyonun içerisinde IEnumerator KirmiziYoluKontrolEt() ve IEnumerator MaviYoluKontrolEt() isminde iki adet “Coroutine” bulunuyor. Burada “WaitUntil” tercihini yaptık.

yield return new WaitUntil(() => dusmanDalgalariYoneticisi.KirmiziHedefErisilebilirMi() == false); kısmında “DusmanDalgalariYoneticisi” scripti içinde yer alan “KirmiziHedefErisilebilirMi” fonksiyonu false dönene kadar çalışmaya devam etsin ve eğer sonuç false olursa alt satırındakiler çalışmaya devam etsin demiş oluyoruz. Kısmen “WaitUntil” bu işe yarıyor. Aslında bir nevi “While” döngüsü gibi düşünebilirsiniz.

Burada sonuç false olursa yol kapalı anlamına geldiği için Destroy(kurulanKule); ile eklenen objeyi siliyoruz. Tabi bu “Coroutine” yapısını içeren “YolDurumunuKontrolEt” fonksiyonunu da kule kurulum işlemi sırasında çağırmalıyız ki kontrollerini yapabilsin.

Bunun için yine “MenuYoneticisi” C# script dosyamızın içinde yer alan birinciKuleSecimi(), IkinciKuleSecimi(), UcuncuKuleSecimi() ve EngelSecimi() fonksiyonlarının içerisine aşağıdaki gibi YolDurumunuKontrolEt(); fonksiyonunu ekiyoruz.

public void birinciKuleSecimi()
{
    zemin.ZeminiSeciliYap();

    YolDurumunuKontrolEt();

    kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(birinciKule, pozisyon);
    if (kurulanKule != null) KurulumMenusuGizle();
}

public void IkinciKuleSecimi()
{
    zemin.ZeminiSeciliYap();

    YolDurumunuKontrolEt();

    kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(ikinciKule, pozisyon);
    if (kurulanKule != null) KurulumMenusuGizle();
}

public void UcuncuKuleSecimi()
{
    zemin.ZeminiSeciliYap();

    YolDurumunuKontrolEt();

    kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(ucuncuKule, pozisyon);
    if (kurulanKule != null) KurulumMenusuGizle();
}

public void EngelSecimi()
{
    zemin.ZeminiSeciliYap();

    YolDurumunuKontrolEt();

    kurulanKule = kurulumYoneticisi.SecilenKuleyiKur(engel, pozisyon);
    if (kurulanKule != null) KurulumMenusuGizle();
}

Böylece açılan bir menüden kule seçimi yapıldığında önce kulenin kurulumu gerçekleşecek ve eğer yol kapalıysa kuleyi silecek bir yapı oluşturmuş olduk.

Burada “Destroy” işleminden sonra kurulan kulelerin altında yer alan gri kısımların silinmediğini fark ettim. Bu durumu giderebilmek için “ZeminYoneticisi” C# Script dosyası içerisine aşağıdaki fonksiyonu ekleyeceğiz.

public void ZeminiVarsayilanYap(Vector3 _pozisyon)
{
    GameObject[] secilenZeminler = GameObject.FindGameObjectsWithTag("DoluZemin");

    foreach (var zemin in secilenZeminler)
    {
        if (zemin.transform.position == _pozisyon)
        {
            zemin.GetComponent<Renderer>().material = orjinalMateryal;
            zemin.transform.tag = "Zemin";
        }
    }
}

Haritaya kurulmuş olan tüm kule ve zeminlerin renkleri gri olduğu için düzeltme işlemi yaparken farklı bir yöntem uygulamamız gerekiyor. Önce GameObject[] secilenZeminler = GameObject.FindGameObjectsWithTag("DoluZemin"); yazarak haritada yer alan tüm “DoluZemin” etiketine sahip objeleri GameObject türünde bir array içerisine atıyoruz.

Sonrasına ise foreach (var zemin in secilenZeminler) ile array içinde yer alan tüm elemanları tek tek “foreach” döngüsü ile gezerek, fonksiyona parametre olarak gelen ve kurulan kulenin pozisyon bilgisi içeren bir eleman var mı kontrol ediyoruz.

Eğer aynı pozisyon bilgilerine sahip bir zemin varsa materyalini zemin.GetComponent().material = orjinalMateryal; ile değiştiriyoruz ve zemin.transform.tag = "Zemin"; ile de etiketini “Zemin” olarak tekrar atıyoruz.

ZeminiVarsayilanYap” fonksiyonunu çağırmak içinse “YoldurumunuKontrolEt” fonksiyonuna dönerek aşağıda işaretlenen satırları ekliyoruz.

public void YolDurumunuKontrolEt()
{
    StartCoroutine(KirmiziYoluKontrolEt());
    StartCoroutine(MaviYoluKontrolEt());

    IEnumerator KirmiziYoluKontrolEt()
    {
        yield return new WaitUntil(() => dusmanDalgalariYoneticisi.KirmiziHedefErisilebilirMi() == false);
        Vector3 silinenKulePozisyonu = kurulanKule.transform.position;
        zemin.ZeminiVarsayilanYap(silinenKulePozisyonu);
        Destroy(kurulanKule);
    }

    IEnumerator MaviYoluKontrolEt()
    {
        yield return new WaitUntil(() => dusmanDalgalariYoneticisi.MaviHedefErisilebilirMi() == false);
        Vector3 silinenKulePozisyonu = kurulanKule.transform.position;
        zemin.ZeminiVarsayilanYap(silinenKulePozisyonu);
        Destroy(kurulanKule);
    }
}

Böylece yol kapalı olduğu için silinen kuleden sonra kulenin bulunduğu zemin objesi de ilk haline döndürülmüş olacak. Eğer adımları başarılı bir şekilde gerçekleştirdiyseniz aşağıdaki gibi bir sonuç almanız gerekiyor.

Kulelerin hedefe giden yolu kapatmasının engellenmesi