Unity EventSystem İle Multiplatform Mouse/Parmak Input’u Almak

Yayınlandı: 21 Mart 2020 yasirkula tarafından Oyun Tasarımı, UNITY 3D içinde

Tekrardan merhabalar,

Bu derste, Unity‘nin EventSystem sisteminden faydalanarak hem PC’de hem de dokunmatik ekranlarda çalışan mouse/parmak input’u yazmayı göreceğiz. Bu sayede, PC’de Input.GetMouseButton ve mobilde Input.GetTouch kullanmak yerine, tek bir kod ile her iki platformu da destekleyeceğiz ve yazdığımız kod otomatik olarak multi-touch destekliyor olacak.

Hazırsanız başlayalım!

Unity’nin UI sistemini kullandıysanız, Hierarchy’de otomatik olarak oluşan EventSystem objesini büyük olasılıkla fark etmişsinizdir. Bu obje, UI sistemine hem PC’de hem de mobilde input gitmesini sağlar. Bu sayede, UI’ınızdaki butonlar her iki platformda da otomatik olarak düzgün çalışır. İlaveten, EventSystem multi-touch desteklediği için, dokunmatik ekranlarda aynı anda birden çok butona dokunmak mümkündür. Birazdan göreceğimiz üzere, biz de dilersek EventSystem’in multi-platform input’undan faydalanabiliriz ve bu esnada tüm ağır işi arkaplanda EventSystem objesi halleder.

EventSystem ile input alabilmek için 2 şeye ihtiyacımız var: input’u alacak bir UI objesine ve bu input’u işleyecek bir script’e. Bunların ilki çok basit: GameObject-UI-Image ile bir Image objesi oluşturun, bu objeyi tüm ekranı kaplayacak şekilde genişletin ve Color değerinin alpha’sı (saydamlık) ile oynayarak objeyi görünmez yapın:

Sadece Raycast Target‘ı açık objeler EventSystem’dan input alabilir, o yüzden bu seçeneği açık bırakmamız önemli.

Gelelim Image objemizin aldığı input’u script vasıtasıyla işlemeye. Burada işin güzel yanı, ne tarz input’ları dinleyebileceğimize karar verebilmemiz. Örneğin sadece ekrana tıklama input’u okuyabileceğimiz gibi, istersek ekranda parmağın kaydırılması input’u da okuyabiliriz.

Basit bir örnekle başlayalım. InputDers adında yeni bir C# script oluşturup bunu Image objesine component olarak verin:

using UnityEngine;
using UnityEngine.EventSystems;

public class InputDers : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
	public void OnPointerDown( PointerEventData eventData )
	{
		Debug.Log( "Parmak dokundu: " + eventData.position );
	}

	public void OnPointerUp( PointerEventData eventData )
	{
		Debug.Log( "Parmak kaldırıldı: " + eventData.position );
	}
}

Oyunu çalıştırıp ekrana birkaç defa dokunun. Her ekrana dokunuşunuzda konsola “Parmak dokundu” yazacak ve yanında da parmağın ekranın neresine dokunduğu yazacak. Benzer şekilde, ekrandan parmağınızı her kaldırdığınızda da konsolda “Parmak kaldırıldı” yazacak.

Script’te göze çarpan 2 kısım var: IPointerDownHandler ve IPointerUpHandler interface‘leri ile OnPointerDown ve OnPointerUp fonksiyonları. Bu interface’lere erişebilmek için kodun başına using UnityEngine.EventSystems; yazmak zorundayız. OnPointerDown, IPointerDownHandler interface’inin fonksiyonu iken OnPointerUp ise IPointerUpHandler’ın fonksiyonu. Unity’nin UI sistemi, bu iki interface’ten de otomatik olarak haberdardır; Image objesine mouse/parmak (pointer) ile dokununca otomatik olarak OnPointerDown fonksiyonunu, pointer ekrandan çekilince de otomatik olarak OnPointerUp fonksiyonunu çağırır.

EventSystem’dan input aldığımız fonksiyonlar parametre olarak bir PointerEventData objesi alırlar. Bu parametre, o UI objesi ile etkileşime geçen mouse/parmak (pointer) hakkında detaylı bilgi barındırır. Örnek kodda PointerEventData’nın position değeri vasıtasıyla, pointer’ın ekrandaki konumuna eriştik. PointerEventData’nın sahip olduğu diğer önemli değişkenler ise şunlardır:

  • button: eğer etkileşime mouse imleci ile geçtiysek, hangi mouse butonunun basılı olduğunu depolar: sol (Left), orta (Middle) veya sağ (Right). Parmak ile etkileşime geçerken bu değer Left’tir
  • delta: son iki frame arasında pointer’ın ne yönde ve kaç piksel hareket ettiğini Vector2 olarak döndürür
  • pointerId: her mouse butonu ve her parmak için eşsiz bir int değer (id) döndürür. Dokunmatik ekranlarda bu değer Input.GetTouch().fingerId ile aynıdır
  • pressPosition: nasıl position pointer’ın ekrandaki mevcut konumunu döndürüyorsa, bu değişken de pointer’ın ekrana ilk dokunduğu koordinatı Vector2 olarak döndürür. position‘dan bu değeri çıkararak, pointer’ın ekranda toplamda kaç piksel hareket ettiği bulunabilir

Şimdi de dilerseniz EventSystem’ın sunduğu diğer event’ler nelermiş onlara bakalım:

  • IPointerClickHandler: Image objesine tıklayınca (pointer ile dokunup ardından dokunmayı kesince) OnPointerClick fonksiyonunu çalıştırır
  • IBeginDragHandler: objeye dokunup pointer’ı bir miktar hareket ettirince OnBeginDrag fonksiyonunu çalıştırır. Pointer kaç piksel kaydırılınca bu fonksiyonun çağrılacağını, EventSystem objesindeki Drag Threshold değişkeni belirler
  • IDragHandler: IBeginDragHandler çalıştıktan sonra, pointer her hareket ettiğinde OnDrag fonksiyonunu çalıştırır
  • IEndDragHandler: IBeginDragHandler çalıştıktan sonra, pointer ekrandan kaldırıldığında OnEndDrag fonksiyonunu çalıştırır
  • IPointerEnterHandler: bir pointer Image objesinin üzerine ilk defa geldiğinde OnPointerEnter fonksiyonunu çalıştırır
  • IPointerExitHandler: Image objesinin üzerindeki bir pointer, Image’ın sınırları dışına çıktığında OnPointerExit fonksiyonunu çalıştırır. Pointer Image’ın sınırları dışına çıktıktan sonra tekrar Image’ın sınırları içine girerse, IPointerEnterHandler tekrar çalışır

Dilerseniz bu yeni bilgiler ışığında örnek kodumuzu biraz güncelleyelim:

using UnityEngine;
using UnityEngine.EventSystems;

public class InputDers : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
	public void OnBeginDrag( PointerEventData eventData )
	{
		Debug.Log( "Dokunan parmak hareket etmeye başladı: " + eventData.pointerId + " " + eventData.position );
	}

	public void OnDrag( PointerEventData eventData )
	{
		Debug.Log( "Parmak hareket ediyor: " + eventData.pointerId + " " + eventData.delta );
	}

	public void OnEndDrag( PointerEventData eventData )
	{
		Debug.Log( "Parmak dokunmayı kesti: " + eventData.pointerId + " " + eventData.position );
	}
}

Eğer kodu dokunmatik ekranda test ederseniz, ekranda hareket eden her parmak için farklı bir pointerId ile Debug.Log çalıştırıldığını göreceksiniz.

Bazen script’in aynı anda sadece bir parmağı takip etmesini isteyebilirsiniz. Mesela üstteki örnekte, ekranda birden çok parmak hareket etse bile, sadece tek bir parmak için OnDrag‘in çalışmasını isteyebilirsiniz. Bunu yapmak oldukça basit:

using UnityEngine;
using UnityEngine.EventSystems;

public class InputDers : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
	private int takipEdilenPointerId;

	public void OnBeginDrag( PointerEventData eventData )
	{
		// Sadece bu pointer'ın OnDrag'i çalıştırmasını sağla
		takipEdilenPointerId = eventData.pointerId;

		Debug.Log( "Dokunan parmak hareket etmeye başladı: " + eventData.pointerId + " " + eventData.position );
	}

	public void OnDrag( PointerEventData eventData )
	{
		if( eventData.pointerId != takipEdilenPointerId )
		{
			// Bu parmak, takip ettiğimiz parmak değil. Fonksiyondan direkt çık (OnDrag'i çalıştırma)
			return;
		}

		Debug.Log( "Parmak hareket ediyor: " + eventData.pointerId + " " + eventData.delta );
	}

	public void OnEndDrag( PointerEventData eventData )
	{
		if( eventData.pointerId != takipEdilenPointerId )
		{
			// Bu parmak, takip ettiğimiz parmak değil. Fonksiyondan direkt çık
			return;
		}

		Debug.Log( "Parmak dokunmayı kesti: " + eventData.pointerId + " " + eventData.position );
	}
}

Ekrana son dokunan parmağın pointerId‘sini bir değişkende tutuyoruz ve pointerId değeri bununla eşleşmeyen parmaklar için OnDrag ve OnEndDrag’i çalıştırmıyoruz. Bu kadar basit!

İsterseniz bir başka basit örneğe daha bakalım: Image objesi üzerinde parmağımızı sürükledikçe, sahnedeki bir objenin parmağın olduğu konuma ışınlanmasını ve parmakla beraber hareket etmesini sağlayalım. Bunun için kodu aşağıdaki gibi güncelleyip Inspector’dan Obje değişkenine değer olarak sahnedeki bir 3D objeyi verin:

using UnityEngine;
using UnityEngine.EventSystems;

public class InputDers : MonoBehaviour, IPointerDownHandler, IDragHandler
{
	private Camera kamera;
	private int takipEdilenPointerId;

	public Transform obje;

	private void Awake()
	{
		kamera = Camera.main;
	}

	public void OnPointerDown( PointerEventData eventData )
	{
		// Sadece bu pointer'ın OnDrag'i çalıştırmasını sağla
		takipEdilenPointerId = eventData.pointerId;
		ObjeyiPointeraIsinla( eventData );
	}

	public void OnDrag( PointerEventData eventData )
	{
		if( eventData.pointerId == takipEdilenPointerId )
			ObjeyiPointeraIsinla( eventData );
	}

	private void ObjeyiPointeraIsinla( PointerEventData eventData )
	{
		Vector2 pointerKonum = eventData.position;

		// Objeyi parmağın olduğu konuma, kameradan 5 metre uzakta olacak şekilde ışınla
		obje.position = kamera.ScreenToWorldPoint( new Vector3( pointerKonum.x, pointerKonum.y, 5f ) );
	}
}

EventSystem ile input almanın beraberinde getirdiği birkaç avantaj da bulunmakta:

  • input’u bir UI objesi vasıtasıyla aldığımız için, bu UI objesinin üzerinde yer alan ve Raycast Target‘ı açık olan başka bir UI objesine tıklayınca, input bizim script’imize ulaşmaz. Yani örneğin ekrandaki bir butona tıklayınca bizim script’imizdeki OnPointerDown çalıştırılmaz; pointer’ın dokunduğu noktada başka bir UI objesi yoksa ancak o zaman çalıştırılır
  • ekranın sadece belli bir kısmından (örneğin sağ yarısı) input alabiliriz. Bunun için Image objesinin konum ve boyutuyla oynamamız yeterli
  • yazının başında dediğim gibi, bu sistem otomatik olarak multi-touch desteklediği için, bir parmak bizim script’imize input sağlarken aynı anda başka bir parmak da ekrandaki bir butona tıklayabilir

Yazıyı noktalamadan önce ufak bir noktaya değinmek istiyorum: şu anda input’u bir Image objesiyle alıyoruz. Her ne kadar bu Image objesi görünmez olsa da, maalesef halen GPU tarafından işlenerek overdraw sıkıntısına yol açmakta. Bunun çözümü ise çok basit:

  1. GorunmezImage adında yeni bir C# script oluşturun:
using UnityEngine.UI;

// Credit: http://answers.unity.com/answers/1157876/view.html
public class GorunmezImage : Graphic
{
	public override void SetMaterialDirty() { return; }
	public override void SetVerticesDirty() { return; }

	protected override void OnPopulateMesh( VertexHelper vh )
	{
		vh.Clear();
		return;
	}
}
  1. Image objesinden Image component’ini kaldırın ve onun yerine GorunmezImage component’ini verin (bu component GPU tarafından işlenmez ve o yüzden fillrate sıkıntısını oluşturmaz):

BONUS: EventSystem Script’lerinin 2D ve 3D Objelerde Çalışmasını Sağlamak

2D Sprite‘larınızda veya 3D objelerinizde OnMouseDown, OnMouseDrag ve OnMouseUp fonksiyonlarını kullanıyorsanız, bu fonksiyonların multi-touch desteklemediğini fark etmiş olabilirsiniz. Derste bahsettiğim OnPointerDown ve OnPointerUp gibi multi-touch destekleyen fonksiyonların bu objelerde de çalışmasını istiyorsanız, yapmanız gereken basit:

  • sahnenizde halihazırda bir EventSystem objesi yoksa, GameObject-UI-Event System ile yeni bir tane oluşturun
  • kameranıza Physics Raycaster (3D objeler için) veya Physics 2D Raycaster (2D Sprite’lar için) component’lerinden birini verin. Bu component’ler raycast bazlı çalıştığı için, sahnede çok fazla obje varsa performansı olumsuz etkileyebilirler. Bu sorunu minimuma indirgemek için, component’in değişkenleriyle oynayabilirsiniz:
    • Event Mask: hangi layer’lardaki objeler için OnPointerDown gibi fonksiyonların çalıştırılacağını belirler
    • Max Ray Intersections: raycast ışınlarının maksimum kaç objeye isabet edebileceğini belirler. Örneğin A objesi sahnede B objesinin arkasında kalıyorsa ve OnPointerDown fonksiyonu A objesinde yer alıyorsa, raycast’in hem B objesine hem de onun arkasındaki A objesine değebilmesi için bu değer minimum 2 olmalıdır. Bu değer 0 olursa bir limit yoktur ama en iyi performans için, değerini mümkünse 1, yoksa 1’e yakın bir rakam verin
  • OnPointerDown vs. fonksiyonlara sahip olan script’inizi, istediğiniz 2D Sprite’lara veya 3D objelere verin (bu objelere Image veya GorunmezImage gibi başka bir component vermenize gerek yok, onun yerine bu objelerde Collider2D/Collider olduğundan emin olun)

Böylelikle dersi noktalıyorum. Sonraki derslerde görüşmek üzere!

yorum
  1. Barış Biltekin dedi ki:

    Unity’nin eski event systemi ile yeni event systemi arasında input latency ile ilgili bir iyileştirme var mı?

    • yasirkula dedi ki:

      Input System’i pek deneme yapma şansım olmadı ama ben normal UI sisteminde de şimdiye kadar input gecikmesi yaşamamıştım.

      • Barış Biltekin dedi ki:

        Genelde oyunlarda input gecikmesi algılanamaz fakat bir müzik oyunu geliştiriyorsanız bu konu gerçekten önemli oluyor çünkü hali hazırda cihazlarda işitsel gecikme var ve bunun üstüne birde input gecikmesi eklenince gecikme insan kulağı tarafından algılanabilir hale geliyor. İşitsel gecikme değeri 20ms üzerine çıktığında insan kulağı gecikmeyi algılayabiliyor. Bende çok emin değilim yeni event sistemini mi kullanmalıyım yoksa eskisi ile devam mı etmeliyim. En kötü deneme yanılma bir sonuç elde etmeye çalışırım.

      • yasirkula dedi ki:

        Ben bu durumda AudioSource’tan şüphelenirdim. Android’de seslerde gecikmeler olmasına ben de denk gelmiştim, bu konuyla ilgili google’da yabancı konu başlıkları da bulabilirsiniz. UI’la etkileşime girerken bir gecikmeye ise denk gelmedim. Butona tıklayınca ses çalıyorsa, parmağı ekrana basınca değil de parmağı ekrandan çekince sesin çaldığını hatırlatmak isterim.

      • Barış Biltekin dedi ki:

        Yok audiosource ile bir müzik oyunu yapılması imkansız ki işitsel gecikme 400 ms’ye kadar çıkıyor. Ben android için Oboe kullanıyorum. Bu sayede Android Api 8.1 ‘den sonra AAudio, daha düşük versiyonlarda OpenSL ES kullanıyor. Oyun AAudio kullandığında işitsel gecikme 10-15ms’ye kadar düşüyor. Elimde eski cihaz olmadığından OpenSL ES’i test edemiyorum bu yüzden input gecikmesini de olabildiğince azaltmak istiyorum muhtemelen mevcut android cihazların 90% ‘nında sorun çıkmaz ancak low-end cihazlarda olabilir onlarda piyasadan silinmek üzere. Büyük olasılıkla boşuna endişe ediyorum ama yine de danışmak istedim, teşekkürler

      • yasirkula dedi ki:

        Oboe’yi bilmiyordum, teşekkür ederim. Input gecikmesi hissetmenizin bir sebebi de framerate düşüklüğü veya framerate’in 60’a sınırlanması olabilir çünkü tıklamalar frame’ler içerisinde gerçekleşiyor. Aklıma başka bir şey maalesef gelmiyor, dediğim gibi ben o gözle bakmadığım için kendim gecikme hissetmemiştim.

  2. onur dedi ki:

    Hocam 3d obje ile 2d görüntülü oyun yapıyorum klavye ile kontrolü yaptım fakat mobil kontrolünü yapamadım yardımcı olursan sevinirim

  3. Bedirhan dedi ki:

    Hocam sorunu çözdüm belki aynı sorunu yaşayan arkadaşlar da okur belki
    transform.position = Input.mousePosition yerine transform.position = eventData.position yazılması gerekiyormuş. Tekrar çok teşekkür ediyorum ilginiz için.

  4. Bedirhan dedi ki:

    Hocam peş peşe yazdığım için kusuruma bakmayın, ben yapamadım maalesef.
    Yani amacım 2D pong tarzı mobile bir oyun, her iki oyuncuyu da hareket ettirebiliyorum fakat sorun ikisini de aynı anda hareket ettirdiğimde oluyor.
    Normalde objelerim Canvas içinde değildi, dediğiniz gibi Canvas içinde image oluşturdum ve objelerimin özelliklerini bu image’lara verdim fakat yine yapamadım.

    • yasirkula dedi ki:

      Bir de yorumumda bahsettiğim yöntem ile, canvas içinde değilken deneyin.

      • Bedirhan dedi ki:

        Yine yapamadım hocam.

      • Bedirhan dedi ki:

        Sanırım sorun Main Camera içi objelerde kullanılan hareket kodlarından dolayı. UI objesi olmadığı için hareket etmiyor diye düşünüyorum hocam.

      • Bedirhan dedi ki:

        Hocam tekrar merhabalar, Canvas içindeki 2 farklı objeme 2 farklı script atadım. OnPointerUp fonksiyonu içine transform.position = Input.mousePosition kodunu yazdım. İki objemde ayrı ayrı hareket ediyor. Tek problem şu;
        Aynı anda birini yukarı birini aşağı sürüklüyorum, bir objem tam sürüklediğim yere gidiyor fakat diğeri tam sürüklediğim yere gitmiyor.

      • yasirkula dedi ki:

        Verdiğiniz bilgiler ışığında aklıma gelen bir şey yok maalesef.

  5. Bedirhan dedi ki:

    Hocam iyi akşamlar,
    Scripti aynen kopyaladım sonrasında 2 tane 2d sprite oluşturdum OnMouseDown ve OnMouseUp fonksiyonlarıyla rb.velocity ile hareket ettirdim. Fakat benim istediğim ikisine de aynı anda dokunup hareket ettirmekti. Yani birine dokunup hareket ettirdiğimde ve diğerine dokunduğumda 2. objem hareket etmiyor. Ne yapmam lazım? Teşekkür ederim.

    • yasirkula dedi ki:

      İlk attığım script’teki OnPointerDown’ı OnMouseDown, OnPointerUp’ı da OnMouseUp olarak düşünebilirsiniz. Ancak bu script sadece UI’da çalışır, Sprite’larınıza dokununca çalışmasını istiyorsanız sahnenizde bir “GameObject-UI-Event System” objesi oluşturun (halihazırda yoksa), kameranıza Physics 2D Raycaster component’i verin ve Sprite’larınızda Collider 2D olduğundan emin olun.

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.