Herkese selam, bu yazıda Unity Coroutine Nedir? Nasıl kullanılır? sorularını cevaplayacağım. Unity Coroutine aslında bildiğimiz fonksiyon yapısına sahiptir, tek fark ise bu fonksiyonlar içerisinde bekletme işlemini gerçekleştirebiliyor oluşumuzdur. Update veya FixedUpdate fonksiyonlarını düşündüğümüzde, bunlar her karede çağrılan fonksiyonlardır. Coroutine fonksiyonları ile bizim yaptığımızda bu sürekli çağrılma durumu yerine, verdiğimiz sürelerde beklemeye geçip, komutlarımızın çalışmasını sağlamaktadır. 

Belirli aralıklarla ışıkları yakıp söndürmek, silahın belirli aralıklarla ateşlenmesini sağlamak gibi işlemler için idealdirler. Yapısına baktığımızda, bir nevi uydurma-paralel bir akış oluşturarak ana akış (main thread) dışı çalışıyor gibi gözükebilirler. Ancak multi-threading olayıyla alakası bulunmamakta. Bunu baştan bilmenizde ve karıştırmamanızda fayda var. 

Bahsettiğim gibi, ana akış üzerinde çalışmaya devam eden fonksiyonlar aslında. Ve oyun objelerine bağlıdırlar. Yani bir gameobject üstündeki script ile oluşturulan coroutine, obje yok olduğu anda yok olacaktır. Öte yandan, bir coroutine çalışıyorken, bulunduğu bileşeni deaktif etsek bile, fonksiyon tamamlanana kadar çalışmaya devam edecektir. 

Peki bu bekleme olayını oluşturmamızı sağlayan şey nedir? Aslında, sadece burada dönüş tipi olarak IEnumerator kullanmamız yetiyor. Void tipli fonksiyonlar dışındaki fonksiyonlarda bir değer döndürmemiz gerekmekte normalde. Burada da IEnumaretor tipi ile değer döndürdüğümüzde, aslında sisteme “dur bakayım, şu kadar süre bekleyeceksin” demekteyiz. Ama aklınız karışmasın, bekleme derken bir sayı değeri vs döndürmüyoruz burada. IEnumerator tipinde değişken döndürüyoruz. 

Önemli bir nokta da, eğer bu bekletme işlemini sağlamak istiyorsak, fonksiyonları StartCoroutine isimli fonksiyon aracılığıyla çağırmamız gerekli. Eğer direkt oluşturduğunuz IEnumerator tipindeki fonksiyonu çağırırsanız, bekleme işlemi çalışmayacaktır. Normal bir fonksiyon nasıl işliyorsa, aynı şekilde işleyecektir komutlarımızı. Aşağıdaki örnek kod üzerinde temel mantığı görebilirsiniz. Dönüş tipine sahip olduğu için Coroutine tipinde bir değişkende tutmamız da mümkün oluyor.

using System.Collections;
using UnityEngine;

public class CoroutineBasitOrnek : MonoBehaviour {

    Coroutine test;

    IEnumerator testEnumarator;
    void Start () {
        //Standart başlatma
        test = StartCoroutine (TestCoroutine ());
        /*isterseniz 2 farklı yöntemle daha oluşturabilirsiniz;
        //string ile başlatma
        test = StartCoroutine ("TestCoroutine");
        //IEnumerator ile başlatma
        testEnumarator=TestCoroutine();
        test=StartCoroutine(testEnumarator);
        */
    }

    void Update () {
        if (test == null) {
            test = StartCoroutine (TestCoroutine ());
        }
    }

    IEnumerator TestCoroutine () {
        Debug.Log ("Coroutine Başladı:" + Time.time);
        yield return new WaitForSeconds (2f);
        Debug.Log ("Coroutine Bitti:" + Time.time);
        yield return null;
    }
}

Gördüğünüz gibi, return komutu ile bir değer döndürüyoruz burada. Öncesinde ve sonrasında Log komutları ile zamanı ekrana yazdırıyoruz. Şayet henüz çalıştırmadıysanız, kodu alıp bir script oluşturarak, bir objenize atın ve çalıştırın. İşlem sırasının nasıl ilerlediğini böylece daha iyi anlayabilirsiniz diye düşünüyorum. 

Örnek kod şu an baktığımızda işlevsiz gibi ama çalışma prensibini anlatmak adına yeterli. Öncelikle Coroutine tipinde bir değişkenimiz var, bunu Start içerisinde tanımlıyoruz ve StartCoroutine fonksiyonundan yararlanarak fonksiyonu çalıştırıyoruz. Sonrasında update içerisinde sürekli olarak değişkenimizi kontrol ediyoruz. Eğer null değer geldiyse, ki fonksiyon bitiminde bu değeri döndürmekteyiz, işlem bittiğini anlıyor ve tekrar çalıştırıyoruz. 

Bir değişkende tutup null olup olmadığını kontrol etmek tabii ki bir yöntem. Ama bana çok sağlıklı gelmeyen bir yapı bu. Bunun yerine farklı değişkenler kullanıp Coroutine içerisinde başlangıç ve bitişte bunu değiştirmek daha mantıklı. Yazının devamındaki örnekte de bunu yapıyor olacağız zaten.

Bu arada, Time.time ile oyunda başlangıçtan itibaren geçen zamanı elde ediyoruz. Time sınıfı ile daha fazla bilgi edinmek isterseniz “Unity Time Sınıfı” yazımızı okuyabilirsiniz. WaitForSeconds fonksiyonuna ve null değer döndürmeye yazının devamında detaylı gireceğim ama şimdilik verdiğimiz saniye kadar bekleme yaratmamızı sağladığını bilmeniz yeterli. 

Kodda da gördüğünüz gibi, return komutunun öncesine de, sonrasına da farklı komutlar ekleyebiliyoruz. Normal şartlarda, bir fonksiyonda return komutu sonrasında yazdığımız şeylere erişilmez, o komutlar çalışmaz. Hatta editörünüz genelde bunu size uyarı olarak da gösterir. Coroutine yapısında ise işler biraz daha farklı. Belirttiğimiz süre geçtikten sonra, fonksiyonumuz kaldığı yerden çalışmaya devam ediyor. Temelde Unity Coroutine Nedir? sorusunun cevabı da bu aslında.

Unity Coroutine Kullanımı 

Unity Coroutine Kullanımı için yukarıda verdiğim örnek tek başına yeterli kalmayacaktır. İşleyişi biraz daha iyi anlayabilmeniz adına, bir döngü oluşturup objenin verdiğimiz konuma ilerlemesini sağlayan bir yapı oluşturalım. Lerp komutu basitçe verdiğimiz 2 nokta arasında pozisyon elde etmemizi sağlamakta. Daha detaylı bilgiyi “Unity Vector3 Nedir?” yazımızdan elde edebilirsiniz.

using System.Collections;
using UnityEngine;

public class CoroutineMovementWithWhile : MonoBehaviour {

    Vector3 hedefKonum = new Vector3 (3, 0, 0);

    void Update () {
        if (Input.GetKeyDown (KeyCode.Space)) {
            StartCoroutine (Hareket (hedefKonum));
        }
    }

    IEnumerator Hareket (Vector3 hedef) {
        float yol = 0;
        Vector3 baslangic = transform.position;
        while (yol <= 1f) {
            transform.position = Vector3.Lerp (baslangic, hedef, yol);
            yol += Time.deltaTime;
            yield return null;
        }
    }
}

Gördüğünüz gibi, döngüde hiçbir bozulma olmadan tekrar tekrar çalışmaya devam ediyor. Burada verdiğim null değeri biraz garip gelmiş olabilir. E hiç bekletmiyor muyuz o halde diye sorabilirsiniz. Aslında yine bekleme işlemi belirtiyoruz ama 1 kare boyunca beklemesini sağlıyoruz gibi düşünebilirsiniz. Detaylı bilgi yazının devamında.

Şimdi döngüye gelecek olursak, aynı daha önceki örnekte olduğu gibi, sıradaki komutu yerine getiriyor aslında. Hareket gerçekleşiyor, bekleme işlemi sağlanıyor. return komutu sonrasındaki satırda bakıyoruz ki bir döngü içerisindeyiz. Bu yüzden döngü devam etmeli mi etmemeli mi kontrolü yapılıyor yine. Sanki hiçbir bekleme olmamış gibi aynı devam

Belki farketmişsinizdir, hareket devam ederken, yani Coroutine çalışıyorken tekrar space tuşuna basarsak harekette bir sapıtma meydana geliyor. Hali hazırda bu coroutine çalışıyor mu çalışmıyor mu diye kontrol etmemizi sağlayan bir yapı yok. Elle kendimiz oluşturmamız ve kontrolü sağlamamız lazım. 

Ayrıca örneği biraz daha işe yarar hale getirmek adına, WASD tuşlarını kullanarak öne-arkaya, sağa-sola hareket edebilir bir yapı oluşturacağım sizler için. Öncesinde adım adım ekleyeceklerimi yazacağım, kodun tamamını görmeden sizin de bunu yapmaya çalışmanızı istiyorum. En sonunda kodu vereceğim ancak birazcık kafadaki çarkları çalıştırmanın zararı olmaz 🙂 

Öncelikle Coroutine ile sağladığımız hareket işlemi gerçekleşiyor mu ya da gerçekleşmiyor mu bunu bir değişkende tutmakta fayda var, böylece kontrolü sağlayabiliriz. Bunun için bir enum yapısı kurabilirsiniz, farklı hareket durumlarını kontrol etmek için. Ancak ben şu an bool tipinde “hareketEdiyor” isimli bir değişken oluşturacağım. Hareket fonksiyonu içerisinde en başta bu değeri true hale getirip, hareket işlemi bitince de false hale getirebiliriz. Ve tabii ki, space tuşuna basıldığında “hareketEdiyor” false değere sahip ise kodumuzu çalıştıracağız. 

Bu adımları gerçekleştirip çalıştırdığınızda, space tuşuna arka arkaya bassanız bile çalışmadığını göreceksiniz. Eğer hâlâ çalışıyorsa, bir nokta gözünüzden kaçmış olabilir.

Farklı yönler kısmı da gayet basit aslında. Her bir tuş için kontrol sağlayabiliriz. Ve hedef konum yerine transform.forward, transform.right komutlarından yararlanmamız mümkün. Ancak bu yöntemle yön kısmında karışıklık da olabilir. Çünkü kendi sağı veya ileri yönünü elde ediyoruz transform üzerinden. Peki genel koordinat sistemine göre sağ ve sol istersek ne kullanacağız? Veya farklı büyüklükler? Bunun için de Vector3.forward veya Vector3 left kullanmamız mümkün. Tamamen tercihinize göre bu ikisi arasında seçim yapabilirsiniz.

Ben Vector3 kullanacağım için, öncelikle varolan pozisyonuna gitmek istediğim vector3 değerini eklemek gerekiyor. Bunu isterseniz Coroutine içerisine değişken olarak verirken hesaplatabilirsiniz veya Coroutine içerisinde hesaplatabilirsiniz. Tamamen size kalmış bu tercih. 

Bu işlemleri adım adım yaptığımızda, kodumuzun son hali aşağıdaki gibi olacaktır. Dediğim gibi, ben Vector3 altyapısını kullanarak yaptım. Ancak siz transform değerlerini kullanarak bunu gerçekleştirebilirsiniz. Hatta pratik amacıyla kesinlikle öneririm. Bu arada Transform bileşenine dair daha fazla bilgi edinmek isterseniz de “Unity Transform Nedir?” yazımıza göz atabilirsiniz.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CoroutineMovementWithWhile : MonoBehaviour {

    Vector3 hedefKonum = new Vector3 (3, 0, 0);
    bool hareketEdiyor = false;

    void Update () {
        //eğer hareket etmiyorsa
        if (!hareketEdiyor) {
            if (Input.GetKeyDown (KeyCode.W)) {
                StartCoroutine (Hareket (Vector3.forward)); //ileri
            }
            if (Input.GetKeyDown (KeyCode.S)) {
                StartCoroutine (Hareket (Vector3.back)); //geri
            }
            if (Input.GetKeyDown (KeyCode.D)) {
                StartCoroutine (Hareket (Vector3.right)); //sağ
            }
            if (Input.GetKeyDown (KeyCode.A)) {
                StartCoroutine (Hareket (Vector3.left)); //sol
            }
        }
    }

    IEnumerator Hareket (Vector3 hedef) {
        float yol = 0;
        Vector3 baslangic = transform.position;
        hedef += baslangic; //başlangıç ve hedefi topluyoruz. Böylece global değil local, obje pozisyona göre bir hedef yaratmış oluyoruz
        while (yol <= 1f) {
            transform.position = Vector3.Lerp (baslangic, hedef, yol);
            yol += Time.deltaTime;
            yield return null;
        }
    }
}

Bu haliyle basit bir örnek oluşturduk aslında. Buna dair ufak bir ödev vereceğim size. Her şey iyi güzel çalışıyor ama, hareket sanki birazcık yavaş. Bunu kontrol edebiliyor olsak çok daha iyi olurdu. Bunun için ne yapabiliriz bi düşünmenizi istiyorum. İpucu olarak da şunu belirteyim, objenin konumunu belirlememizi sağlayan şey “yol” isimli değişken. 0 olduğunda ilk pozisyonda, 1 olduğunda ikinci pozisyonda bir değer döndürüyor bize. Time.deltaTime ile arttırmanın yanında bir şeyler daha yapsak sanki olacak gibi. Top artık sizde 🙂

Unity Coroutine Nedir? Nasıl Kullanılır? sorularını hallettik, gayet güzel. Bunları çalıştırıyoruz ama bir noktada durdurmamız da gerekebilir. Şimdi Unity Coroutine işlemleri nasıl durdurulur ona bir bakalım.

Unity Coroutine Durdurma

Unity Coroutine Durdurma için 2 fonksiyon bulunmakta. Herhangi bir durumda Coroutine işlemlerini, yazdığınız fonksiyondan önce durdurmak isteyebilirsiniz. Bu fonksiyonlar da tam bu ihtiyacı gidermek için oluşturulmuş durumda. 

StopCoroutine: Verdiğimiz Coroutine nesnesi aracılığıyla bir Coroutine fonksiyonunu durdurmamıza imkan veren kod. Ve evet, Coroutine işlemini bir değişkende tutmak mümkün. Ancak bunun dışında istersek IEnumarator tipinde istersek de string tipinde değişken vererek durdurma işlemini gerçekleştirebiliriz. Bu noktada dikkat etmemiz gereken şey, StartCoroutine ile nasıl başlattığımız. Örneğin string değişken vererek başlattıysak yine string ile bitirmemiz gerekiyor. Aşağıda 3 durum için de başlatma bitirme örneklerini bulabilirsiniz.

using System.Collections;
using UnityEngine;

public class CoroutineDegisken : MonoBehaviour {

    Coroutine hareketCoroutine;
    // Start is called before the first frame update
    void Start () {
        //Coroutine ile başlatma
        hareketCoroutine = StartCoroutine (Ileri ());
    }

    void Update () {
        if (Input.GetKeyDown (KeyCode.Space)) {
            //değişken ile durdurma
            StopCoroutine (hareketCoroutine);
        }
    }

    IEnumerator Ileri () {
        for (;;) {
            transform.position += Vector3.forward * Time.deltaTime;
            yield return null;
        }
    }
}
using System.Collections;
using UnityEngine;

public class CoroutineString : MonoBehaviour {

    void Start () {
        //String değişken ile başlatma
        StartCoroutine ("Ileri");
    }

    void Update () {
        if (Input.GetKeyDown (KeyCode.Space)) {
            //fonksiyon ismi ile durdurma
            StopCoroutine ("İleri");
        }
    }

    IEnumerator Ileri () {
        for (;;) {
            transform.position += Vector3.forward * Time.deltaTime;
            yield return null;
        }
    }
}
using System.Collections;
using UnityEngine;

public class CoroutineIEnumerator : MonoBehaviour {

    IEnumerator hareket;

    void Start () {
        //IEnumaretor değişken ile başlatma
        hareket = Ileri ();
        StartCoroutine (hareket);
    }
    void Update () {
        if (Input.GetKeyDown (KeyCode.Space)) {
            //IEnumarator ile durdurma
            StopCoroutine (hareket);
        }
    }
    IEnumerator Ileri () {
        for (;;) {
            transform.position += Vector3.forward * Time.deltaTime;
            yield return null;
        }
    }
}

StopAllCoroutine : Bileşenimiz içerisinde oluşturduğumuz bütün coroutine işlemlerini durdurmamızı sağlar. 

using System.Collections;
using UnityEngine;

public class CoroutineStopAll : MonoBehaviour {

    [SerializeField]
    float donusHizi = 30, genislik = 3, boyutHizi = 30;
    // Start is called before the first frame update
    void Start () {
        StartCoroutine ("Don");
        StartCoroutine (Boyutlandir ());
    }

    void Update () {
        if (Input.GetKeyDown (KeyCode.Space)) {
            StopAllCoroutines ();
        }
    }
    //Zamanla açıyı arttırarak sin-cos fonksiyonları ile x-y konumlarını değiştirip dairesel hareket sağlıyoruz.
    IEnumerator Don () {
        float aci = 0;
        while (true) {
            aci += Time.deltaTime * donusHizi;
            float posX = Mathf.Sin (aci * Mathf.Deg2Rad);
            float posY = Mathf.Cos (aci * Mathf.Deg2Rad);
            transform.position = new Vector3 (posX, posY, 0) * genislik;
            yield return null;
        }
    }
    //sinüs fonksiyonununda artma azalma durumundan faydalanarak boyutu büyütüp küçültüyoruz
    IEnumerator Boyutlandir () {
        while (true) {
            float buyukluk = Mathf.Sin (Time.time * boyutHizi * Mathf.Deg2Rad);
            buyukluk = Mathf.Abs (buyukluk);
            transform.localScale = Vector3.one * buyukluk;
            yield return null;
        }
    }
}

Örneklerde kullanmış olduğum Mathf sınıfı hakkında daha detaylı bilgiyi “Unity Mathf Sınıfı” yazımızda bulabilirsiniz. Yazının devamında return komutunda kullanabileceğimiz dönüş değerlerine de yer verdim. Muhakkak bakmanızı öneriyorum.

Unity Coroutine Dönüş Değerleri

Unity Coroutine Dönüş Değerleri için Unity bize hali hazırda seçenekler sunmakta. Yazının devamında bunlara değinip kod örneklerini oluşturacağım sizler için. Daha önceki örneklerde de gördüğünüz gibi “yield return new” yazdıktan sonra bu komutları kullanabilirsiniz. Yalnızca null ve break değeri öncesinde “new” kelimesini kullanmanıza gerek yoktur.

WaitForEndOfFrame : Bu değeri kullandığımızda, komutlarımızı karenin sonuna kadar bekletmemizi sağlar. Ancak burada dikkat etmemiz gereken şey, komutta da belirttiği gibi, karenin en sonunda bu işlemin gerçekleşecek olmasıdır. Eğer update veya lateupdate içerisinde bir işlem yapıp, sonrasında Coroutine fonksiyonunuz içerisinde kontrol etmek isterseniz, bu yöntem faydalı olabilir.

    IEnumerator BuyutKucult () {
        while (true) {
            transform.position += Vector3.forward * Time.deltaTime;
            yield return new WaitForEndOfFrame ();
        }
    }

WaitForFixedUpdate : Bir dahaki Fixed Update fonksiyonu çağırılana kadar komutlarımızı bekletmemizi sağlar.

    IEnumerator Hizlandir (float sure) {
        float zaman = 0;
        while (zaman < sure) {
            zaman += Time.deltaTime;
            Vector3 velocity = rigidbody.velocity;
            velocity *= .1f;
            rigidbody.velocity = velocity;
            yield return new WaitForEndOfFrame ();
        }
    }

WaitForSeconds : Oyun zamanında göre belirttiğimiz saniye kadar beklememizi sağlar. TimeScale değerinden etkilenir. Ancak belirtmekte fayda var, çok kesin bir şekilde bu kontrol yapılmaz. Örneğin, 5 saniye beklemesini istediğimizde, tam olarak 5 saniye olduğunda değil de, 5’i çok çok ufak bir şekilde geçtiği anda çalışacaktır.

    IEnumerator AdimAdımIsinla (int adim, Vector3 yon) {
        for (int i = 0; i < adim; i++) {
            transform.position += yon;
            yield return new WaitForSeconds (.1f);
        }
    }

WaitForSecondsRealtime : Oyun zamanından bağımsız şekilde, gerçek zamana göre saniye cinsinden değer vermemizi sağlayan yapıdır.


    IEnumerator Hatirlatici (float sure) {
        while (true) {
            yield return new WaitForSecondsRealtime (sure);
            Debug.Log (sure + " saniye geçti, su içmeyi unutma!!");
        }
    }

WaitUntil : Belirtiğimiz koşul sağlanana kadar beklememizi sağlayan koddur. Mermi==0 olana kadar beklemeye devam et diyebiliriz örneğin.


    IEnumerator MesafeOlcer (Transform hedef, float mesafe) {
        yield return new WaitUntil (() => Vector3.Distance (transform.position, hedef.position) < mesafe);
        Debug.Log (hedef.name + "isimli objeye çok yakınsın");
    }

WaitWhile : Belirttiğimiz durum sağlandığı sürece beklememizi sağlayan koddur. While döngüsü mantığındaki gibi çalışır.


    IEnumerator TakipKontrol (Transform hedef, float mesafe) {
        yield return new WaitWhile (() => Vector3.Distance (transform.position, hedef.position) < mesafe);
        Debug.Log (hedef.name + " isimli obje takip mesafenden çıktı, artık yakalayamazsın");
    }

null : Çalışma mantığı WaitForEndOfFrame fonksiyonuna benzerdir. Ancak aralarındaki fark, çalıştırılma sıraları ile alakalıdır. null değeri verdiğimizde, bir sonraki karede update fonksiyonu çalıştığı anda çalışacaktır. Yani karenin sonunu beklemeyecektir.

break : Özellikle döngüleri kullandığınız işlemlerde, herhangi bir durumda coroutine işlemini durdurmak için kullanabilirsiniz.


    IEnumerator TusKontrol (int beklenecekKaresayisi) {
        for (int i = 0; i < beklenecekKaresayisi; i++) {
            if (Input.GetKeyDown (KeyCode.Space)) {
                Debug.Log ("Doğru zamanda bastın!!");
                yield break;
            }
            yield return null;
        }
        Debug.Log ("Kaçırdın...");
    }

Böylece Unity Coroutine Nedir? Nasıl kullanılır? sorularını cevaplandırmış olduk. Türkçe dökuman çevirilerine devam edeceğiz ancak her zaman dönüp resmi Unity Dokümanlarına bakmakta fayda var 🙂 Yeni yazılarda görüşmek dileğiyle, umarım faydası olmuştur. Görüşmek üzere.

2 YORUMLAR

  1. Merhabalar ben WaitForEndOfFrame kısmını anlayamadım tam olarak. Karenin sonuna kadar beklerden kastınız tam olarak nedir? Şimdi izlediğim bir kursta aşağıda yazdığım şekilde kullanılıyor.
    while (travelPercent < 1)
    {
    travelPercent += Time.deltaTime;
    transform.position = Vector3.Lerp(startPos, endPos, travelPercent);
    yield return new WaitForEndOfFrame();
    }

    • Kareden kastım aslında Frame, yani Frame Per Second(FPS) kavramındaki frame. Oyunlar aslında sürekli olarak bir döngüde çalışan uygulamalar, kare-frame derken de aslında yapılması gereken bütün işlemlerin tamamlandığı ve ekranda gösterildiği çok kısa bir ân denilebilir. Bu kısa anın en sonuna kadar bekleyip, ondan sonra yapman gerekeni yap diyoruz WaitForEndOfFrame ile. Umarım daha açıklayıcı olmuştur. Geç dönüş yaptığım için kusura bakma.

CEVAP VER

Buraya yorumunuzu ekleyin
Buraya adınızı girin