Unity Scroll View’da Çok Sayıda Objeyi Performanslı Bir Şekilde Göstermek

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

Yeniden merhabalar,

Unity‘de bir dizi veriyi UI‘da göstermek için Scroll View kullanmak idealdir (örneğin oyun içi market veya leaderboard). Ancak göstereceğiniz verinin boyutu arttıkça (örneğin leaderboard’da binlerce satır [entry] göstermek isteyebilirsiniz), Scroll View’ın performansı düşecektir çünkü her bir entry için ayrı bir UI GameObject’i oluşturacaksınız. Bu performans sorununu çözmenin yolu, her bir entry için ayrı bir GameObject oluşturmak yerine, sadece ekranda görünen entry’ler için elimizde bir düzine UI GameObject’i tutup onları tekrar tekrar kullanmaktır. Bunun için, diyelim Scroll View’ı aşağı kaydırdıkça, ekranın üst kenarından dışarı çıkan GameObject’leri ekranın alt kenarına ışınlayıp tekrardan kullanabiliriz. Böylece sadece birkaç GameObject ile binlerce entry’i Scroll View’da gösterebiliriz:

Hazırsanız derse başlayalım!

Bu iş için hatırı sayılır miktarda kod yazmak gerekiyor. O yüzden süreci hızlandırmak adına hazır bir plugin kullanacağız. Asset Store’da bu konuda ücretli plugin’ler (1, 2) bulunmakta ama biz GitHub üzerindeki MIT lisanslı ücretsiz plugin’lere bakacağız. Araştırma yaparken şu 3 ücretsiz plugin ile karşılaştım:

  1. https://github.com/sinbad/UnityRecyclingListView
    • (+) kod olarak en temiz bulduğum plugin bu
    • (-) sadece dikey (Vertical) Scroll View’larda çalışıyor
    • (-) Scroll View’daki tüm GameObject’lerin yükseklikleri aynı olmak zorunda (bir obje 50 piksel yüksekliğinde, öbürü 100 piksel yüksekliğinde olamaz)
    • üstteki şartlar sağlandığında bu plugin’i kullanmayı tavsiye ediyorum
  2. https://github.com/MdIqubal/Recyclable-Scroll-Rect
    • (+) yatay (Horizontal) ve grid tabanlı Scroll View’larda da çalışıyor
    • (-) scrollbar desteklemiyor
    • (-) ekranda gözükmekte olan entry’leri refresh etmeyi desteklemiyor (örneğin elimizdeki veri değişirse bu gerekli)
    • (-) tüm entry’leri temizleme (clear) desteği yok
    • (-) ekran çözünürlüğü veya Scroll View’in genişliği değişirse, ekrandaki GameObject’lerin genişliği otomatik olarak değişmiyor. Örneğin telefonu portrait moddan landscape moda geçirirseniz, ekrandaki GameObject’lerin genişliği ufacık kalıyor
    • tüm bu olumsuz noktalardan dolayı, bu plugin’in kullanımından bahsetmeyeceğim
  3. https://github.com/qiankanglai/LoopScrollRect
    • (+) yatay (Horizontal) ve grid tabanlı Scroll View’larda da çalışıyor
    • (+) farklı yüksekliklere sahip GameObject’leri destekliyor
    • (-) kod olarak çok hoş değil. Örneğin kodda SendMessage fonksiyonunu kullanıyor ve UI GameObject’inin prefab’ını Resources klasörüne koymanızı gerektiriyor (ama kodu biraz değiştirerek bu sorunları çözeceğiz)
    • (-) kurulumu biraz uğraştırıyor. Örneğin scrollbar’ı otomatik oluşturmuyor, elle oluşturmanız gerekiyor
    • eğer yatay/grid tabanlı Scroll View kullanıyorsanız veya her bir entry’nin Scroll View’daki yüksekliği farklı olacaksa, bu 3 alternatif içinden mecburen bunu kullanmak zorundasınız

Bu derste 1. ve 3. plugin’lerin kullanımını göreceğiz. Takıldığınız bir yer olursa, dersteki her şeyin bitmiş halini şu linkten indirebilirsiniz: https://www.dropbox.com/s/775levbi6fncl4i/ListViewDemo.unitypackage?dl=0

Derste basit bir oyun içi market yapacağız. Marketteki eşyaların her birinin kendine has bir ismi ve ücreti olacak. Bir eşyaya tıklayarak onu seçebileceğiz. Seçili eşyanın arkaplan rengi kırmızı olacak:

Öncelikle ilk plugin’in kullanımını görelim.

UnityRecyclingListView

Şu linkten plugin’i indirin: https://github.com/sinbad/UnityRecyclingListView/archive/master.zip. İndirdiğiniz zip‘in içindeki UnityRecyclingListView-master\Source klasörünü, Unity projenizin olduğu konumdaki Assets klasörüne çıkarın. Son olarak, zip’teki LICENSE dosyasını da çıkardığınız klasörün içine atın:

Eski bir Unity sürümü kullanıyorsanız, Invalid accessor body =>', expecting;' or `{' gibi birkaç hata alabilirsiniz. Bu hataları çözmek için:

  • get => scrollRect.verticalNormalizedPosition; satırını get { return scrollRect.verticalNormalizedPosition; } olarak değiştirin
  • set => scrollRect.verticalNormalizedPosition = value; satırını set { scrollRect.verticalNormalizedPosition = value; } olarak değiştirin
  • diğer sıkıntılı satırları da benzer şekilde değiştirin

Gelelim plugin’in kullanımına:

  • sahnenizde GameObject-UI-Scroll View ile yeni bir Scroll View objesi oluşturun
  • Scroll View objesinin Horizontal seçeneğini kapatın, Scrollbar Horizontal objesini silin
  • Scroll View’a Recycling List View component’i ekleyin
  • Scroll View/Viewport/Content‘e sağ tıklayıp yeni bir UI-Image objesi oluşturun. Oluşan objenin ismini TestItem olarak değiştirin. Oyun içi marketteki entry’leri, TestItem’ın klonları ile UI’da göstereceğiz. TestItem’ın RectTransform’unu şu şekilde değiştirin:
  • TestItem’a tıklayabileceğimiz için, ona Button component’i ekleyin
  • şimdi elimizdeki veriye göre TestItem’ı kişiselleştirmemiz lazım. Hatırlarsanız elimizdeki veri, marketteki eşyaların isim ve ücretlerinden ibaret. Bunun için TestItem’ın içerisinde şu şekilde 2 Text objesi oluşturun:
  • TestItem adında yeni bir C# script oluşturup içeriğini şu şekilde değiştirin:
using UnityEngine;
using UnityEngine.UI;

public class TestItem : RecyclingListViewItem
{
	public Text isimText;
	public Text ucretText;

	public Button buton;
	public Image arkaplan;

	[HideInInspector]
	public bool itemYeniOlusturuldu = true;
}
  • TestItem’ın hangi component’lerini kod ile güncelleyeceksek, o component’ler için birer değişken oluşturduk. Burada dikkat etmemiz gereken en önemli şey, oluşturduğumuz class’ın RecyclingListViewItem sınıfından türemesi. “itemYeniOlusturuldu” değişkeninin ne işe yaradığını bir sonraki script’te göreceksiniz
  • sahnedeki TestItem objesine, Test Item component’ini verip değişkenlerin değerlerini doldurun:
  • TestItem objesini Project paneline sürükle-bırak yaparak bir prefab‘a çevirin ve ardından sahneden silin. Oluşturduğunuz prefab’ı Recycling List View component’indeki Child Prefab değişkenine değer olarak verin. Aynı component’teki Row Padding değeri, oyun esnasında 2 TestItem objesi arasında kaç piksel boşluk olacağını belirler. Ben onu 0 yaptım. Pre Alloc Height değişkenini 0 olarak bırakabilirsiniz
  • RecyclingListViewTest adında yeni bir C# script oluşturun:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class RecyclingListViewTest : MonoBehaviour
{
	// Oyun içi marketteki eşyaların bilgilerini tutan veri türü
	public class ItemBilgileri
	{
		public string isim;
		public int ucret;

		public ItemBilgileri( string isim, int ucret )
		{
			this.isim = isim;
			this.ucret = ucret;
		}
	}

	public RecyclingListView scrollView;
	private ScrollRect scrollRect;

	// Markette en başta kaç eşya olacağı
	public int ilkItemSayisi = 50;
	// Her Q tuşuna basınca markete kaç yeni eşya geleceği
	public int eklenenItemSayisi = 5;

	// Marketteki tüm eşyaların verisi
	private List<ItemBilgileri> itemBilgileri;
	// Hangi index'teki eşyanın seçili olduğu
	private int seciliItemIndex = -1;

	private void Start()
	{
		scrollRect = scrollView.GetComponent<ScrollRect>();

		// Marketteki eşyaların verisini çek (bu örnekte veriyi rastgele oluşturuyoruz)
		itemBilgileri = new List<ItemBilgileri>( ilkItemSayisi );
		for( int i = 0; i < ilkItemSayisi; i++ )
			itemBilgileri.Add( RastgeleItemOlustur() );

		// Scroll View'ı aşağı-yukarı kaydırdıkça, kameranın görüş alanına giren
		// her yeni entry için ItemiGuncelle fonksiyonunu çağır
		scrollView.ItemCallback = ItemiGuncelle;

		// Markette kaç eşya olduğu bilgisini RecyclingListView'e bildir
		scrollView.RowCount = itemBilgileri.Count;

		// Oyunun başında bazen "Scrollbar Vertical" objesi TestItem'ların içine giriyor,
		// bu satır o sorunu düzeltmeye yarıyor
		scrollRect.horizontalScrollbarSpacing = scrollRect.horizontalScrollbarSpacing;
	}

	private void Update()
	{
		// Her Q tuşuna basınca, eklenenItemSayisi kadar yeni eşyayı markete ekle
		if( Input.GetKeyDown( KeyCode.Q ) )
		{
			for( int i = 0; i < eklenenItemSayisi; i++ )
				itemBilgileri.Add( RastgeleItemOlustur() );

			// RecyclingListView her güncellenişinde, işi garantiye almak için scrollbar'ı
			// en tepeye ışınlıyor. Marketten eşya silersek bu işlemi yapması önemli çünkü
			// aksi taktirde scrollbar'ın olduğu konumdaki eşyalar silinmiş olabilir ve
			// scrollbar artık boşluğa bakıyor olur. Ama biz markete yeni eşya eklediğimiz
			// için, scrollbar'ın olduğu konumdaki eşyaların silinmesi imkansız. Bu durumda
			// boş yere scrollbar'ın en tepeye ışınlanması bizi rahatsız etmekten başka bir
			// işe yaramamakta. Onun için scrollbar'ın en son konumunu ve scroll hızını
			// birer değişkende tut ve RecyclingListView'ı güncelledikten sonra bu değerleri
			// scrollbar'a geri ata
			Vector2 position = scrollRect.content.anchoredPosition;
			Vector2 scrollSpeed = scrollRect.velocity;

			// Markette artık kaç eşya olduğu bilgisini RecyclingListView'e bildir
			scrollView.RowCount = itemBilgileri.Count;

			// Scrollbar'ın en son konumunu ve hızını geri ata
			scrollRect.content.anchoredPosition = position;
			scrollRect.velocity = scrollSpeed;
		}

		// W tuşuna basınca, markette seçili bir eşya varsa Scroll View'ı otomatik olarak o eşyaya odakla
		if( Input.GetKeyDown( KeyCode.W ) && seciliItemIndex >= 0 && seciliItemIndex < itemBilgileri.Count )
			scrollView.ScrollToRow( seciliItemIndex );

		// E tuşuna basınca marketteki tüm eşyaları sil
		if( Input.GetKeyDown( KeyCode.E ) )
		{
			itemBilgileri.Clear();
			scrollView.RowCount = 0;
		}
	}

	// Market için rastgele eşya verisi oluşturmaya yarar
	private ItemBilgileri RastgeleItemOlustur()
	{
		return new ItemBilgileri( "Item " + itemBilgileri.Count, Random.Range( 1, 1000 ) );
	}

	// index'teki eşyanın TestItem'ı kameranın görüş alanına girdi; TestItem'ın içeriğini
	// o index'teki eşyanın verisini gösterecek şekilde güncelle
	private void ItemiGuncelle( RecyclingListViewItem i, int index )
	{
		TestItem item = (TestItem) i;

		// Isim ve Ucret Text'lerini güncelle
		item.isimText.text = itemBilgileri[index].isim;
		item.ucretText.text = itemBilgileri[index].ucret + " TL";

		// Eğer seçili eşya bu eşya ise, TestItem'ın arkaplan rengini kırmızı yap (if),
		// değilse beyaz yap (else)
		if( index == seciliItemIndex )
			item.arkaplan.color = new Color32( 230, 57, 70, 255 );
		else
			item.arkaplan.color = new Color32( 241, 250, 238, 255 );

		// Eğer bu TestItem için ItemiGuncelle fonksiyonu daha önce çağrılmadıysa
		if( item.itemYeniOlusturuldu )
		{
			// Bu kodu sadece 1 kez çalıştır (aşağıdaki kodu birden çok kez çağırmak boş yere performansı etkiler)
			item.itemYeniOlusturuldu = false;

			// TestItem'a her tıklandığında (onClick) ItemeTiklandi fonksiyonunu çağır
			// Delegate'ler hakkında daha fazla bilgi için: https://yasirkula.com/2019/10/29/unity-c-delegate-ve-eventler/
			item.buton.onClick.AddListener( () => ItemeTiklandi( item ) );
		}
	}

	// Bir TestItem'a tıklandı
	private void ItemeTiklandi( TestItem item )
	{
		// Mevcut seçili eşyanın index'ini bir değişkende tut
		int oncekiSeciliItemIndex = seciliItemIndex;

		// Seçili eşya index'ini güncelle. TestItem'a atanan verinin index'ini
		// bulmak için CurrentRow değişkenini kullanmak yeterli
		seciliItemIndex = item.CurrentRow;

		// seciliItemIndex'indeki eşyanın TestItem'ı için ItemiGuncelle fonksiyonunu
		// tekrar çağır (böylece TestItem'ın arkaplanı kırmızı olacak). İlk parametre
		// hangi index'teki TestItem'ın güncelleneceğini, ikinci parametre ise o index'ten
		// itibaren kaç TestItem'ın güncelleneceğini belirler. Biz sadece
		// seciliItemIndex'teki tek bir TestItem'ın güncellenmesini istiyoruz
		scrollView.Refresh( seciliItemIndex, 1 );

		// Eğer daha önceden bir eşya seçili idiyse, o eşyanın TestItem'ını da güncelle
		// (böylece o TestItem'ın arkaplanı kırmızı renkten geri beyaz renge dönecek)
		if( oncekiSeciliItemIndex >= 0 )
			scrollView.Refresh( oncekiSeciliItemIndex, 1 );
	}
}
  • kodda açıklanması gereken her yeri comment’lerle açıklamaya çalıştım. Özetle, oyunun başında RecyclingListView objesinin ItemCallback‘ine kendi fonksiyonumuzu kaydediyoruz ve akabinde RecyclingListView’a elimizdeki verinin kaç entry’den oluştuğunu RowCount ile söylüyoruz. Scroll View scroll oldukça, ItemCallback fonksiyonu yeni entry’ler için otomatik olarak çağrılıyor. Elimizdeki entry’lerin sayısı her değiştiğinde, bu değişikliği RowCount ile tekrardan RecyclingListView’a bildiriyoruz
  • Recycling List View Test component’ini sahnenizdeki istediğiniz bir objeye verin. Component’teki Scroll View değişkenine değer olarak, sahnedeki Scroll View objesini verin
  • oyunu çalıştırın. Test amaçlı Q tuşu ile markete yeni eşyalar ekleyebilir, W tuşu ile seçili eşyaya odaklanabilir, E tuşu ile de marketteki tüm eşyaları silebilirsiniz

LoopScrollRect

Şu linkten plugin’i indirin: https://github.com/qiankanglai/LoopScrollRect/archive/master.zip. İndirdiğiniz zip‘in içindeki LoopScrollRect-master\Assets\Scripts klasörünü, Unity projenizin olduğu konumdaki Assets klasörüne çıkarın. Çıkardığınız klasördeki EasyObjectPool alt klasörünü silin. Son olarak, zip’teki LICENSE dosyasını da çıkardığınız klasörün içine atın:

Bu plugin’in kodunun o kadar güzel olmadığından bahsetmiştim. Kodu düzeltmenin ilk aşaması olarak EasyObjectPool alt klasörünü sildik. Bu yüzden kod şu anda hata verecek. Adım adım plugin’in kodunu düzeltelim/iyileştirelim (yapacağımız kod değişikliklerini açıklamayacağım çünkü plugin’in kaynak kodunu bilmenize gerek yok):

  • LoopScrollPrefabSource script’ini şu şekilde güncelleyin:
using UnityEngine;
using System.Collections.Generic;

namespace UnityEngine.UI
{
	public class LoopScrollRectItem : MonoBehaviour
	{
		[System.NonSerialized]
		public int CurrentRow;
	}

	[System.Serializable]
	public class LoopScrollPrefabSource
	{
		public Transform prefab;
		public int havuzIlkBoyutu = 5;

		private Stack<Transform> havuz;

		public Transform GetObject()
		{
			if( havuz == null )
			{
				havuz = new Stack<Transform>( havuzIlkBoyutu );

				for( int i = 0; i < havuzIlkBoyutu; i++ )
				{
					Transform instance = Object.Instantiate( prefab, null, false );
					instance.gameObject.SetActive( false );

					havuz.Push( instance );
				}
			}

			if( havuz.Count == 0 )
				return Object.Instantiate( prefab, null, false );
			else
			{
				Transform instance = havuz.Pop();
				instance.gameObject.SetActive( true );
				return instance;
			}
		}

		public void ReturnObject( Transform go )
		{
			go.gameObject.SetActive( false );
			go.SetParent( null, false );

			havuz.Push( go );
		}
	}
}
  • LoopScrollDataSource script’ini şu şekilde güncelleyin:
using UnityEngine;

namespace UnityEngine.UI
{
	public abstract class LoopScrollDataSource
	{
		public abstract void ProvideData( Transform transform, int index );
	}

	public class LoopScrollSendIndexSource : LoopScrollDataSource
	{
		private readonly LoopScrollRect scrollView;

		public LoopScrollSendIndexSource( LoopScrollRect scrollView )
		{
			this.scrollView = scrollView;
		}

		public override void ProvideData( Transform transform, int index )
		{
			LoopScrollRectItem item = transform.GetComponent<LoopScrollRectItem>();
			item.CurrentRow = index;
			if( scrollView.ItemCallback != null )
				scrollView.ItemCallback( item, index );
		}
	}

	public class LoopScrollArraySource<T> : LoopScrollDataSource
	{
		private readonly T[] objectsToFill;

		public LoopScrollArraySource( T[] objectsToFill )
		{
			this.objectsToFill = objectsToFill;
		}

		public override void ProvideData( Transform transform, int idx )
		{
			transform.SendMessage( "ScrollCellContent", objectsToFill[idx] );
		}
	}
}
  • LoopScrollRect script’inde şu değişiklikleri yapın:
    • public LoopScrollDataSource dataSource = LoopScrollSendIndexSource.Instance; satırını public LoopScrollDataSource dataSource; olarak değiştirin
    • biraz altındaki dataSource = LoopScrollSendIndexSource.Instance; satırını dataSource = new LoopScrollSendIndexSource( this ); olarak değiştirin
    • private bool m_ContentConstraintCountInit = false; satırını [HideInInspector] public bool m_ContentConstraintCountInit = false; olarak değiştirin
    • boş bir satıra public System.Action ItemCallback; değişkenini ekleyin
    • protected LoopScrollRect() constructor’ının içine şu satırı ekleyin: objectsToFill = null;

Artık koddaki tüm hatalar gitmiş olmalı. Gelelim plugin’in kullanımına:

  • sahnenizde GameObject-UI-Loop Vertical Scroll Rect ile yeni bir Loop Vertical Scroll Rect objesi oluşturun (yatay [Horizontal] scroll istiyorsanız, Loop Horizontal Scroll Rect oluşturun). Kolaylık açısından, oluşan objenin ismini Scroll View olarak değiştirin
  • eğer scrollbar istiyorsanız, Scroll View objesinin içerisinde UI-Scrollbar oluşturun. Loop Vertical Scroll Rect kullanıyorsanız, scrollbar’ın Direction değişkenini Top To Bottom yapın. Ardından scrollbar’ı ekranın kenarına hizalayın. Son olarak da, Scroll View’ın ilgili Scrollbar değişkenine, oluşturduğunuz Scrollbar objesini değer olarak verin
  • Scroll View/Content‘teki Layout Group component’ini şu şekilde güncelleyin:
  1. Loop Vertical Scroll Rect için:
  1. Loop Horizontal Scroll Rect için:
  1. GridLayout kullanacaksanız, mevcut Layout Group component’ini silip yerine Grid Layout Group component’i ekleyin (buradaki Cell Size, TestItem’ların kaç piksel ebatında olacağını belirler):
  1. eğer Grid Layout Group’u Loop Horizontal Scroll Rect ile kullanacaksanız, Start Axis değişkenini Vertical, Child Alignment değişkenini Middle Left, Constraint değişkenini de Fixed Row Count olarak değiştirin
  2. Grid Layout Group kullanacaksanız, son olarak da GridDinamikEbat adında yeni bir C# script oluşturup onu Scroll View/Content objesine component olarak verin (bu script, ekranın çözünürlüğü değişince Grid Layout Group’un satır/sütun sayısını [Constraint Count] otomatik olarak en uygun değere ayarlar):
using UnityEngine;
using UnityEngine.UI;

[RequireComponent( typeof( GridLayoutGroup ) )]
public class GridDinamikEbat : MonoBehaviour
{
	private GridLayoutGroup gridLayout;
	private LoopScrollRect scrollView;

	private void OnRectTransformDimensionsChange()
	{
		if( gridLayout == null )
			gridLayout = GetComponent<GridLayoutGroup>();

		if( scrollView == null )
			scrollView = GetComponentInParent<LoopScrollRect>();

		Vector2 contentBoyutu = ( (RectTransform) transform ).rect.size;

		int yeniHucreSayisi;
		if( gridLayout.constraint == GridLayoutGroup.Constraint.FixedColumnCount )
			yeniHucreSayisi = (int) ( ( contentBoyutu.x - gridLayout.padding.horizontal ) / ( gridLayout.cellSize.x + gridLayout.spacing.x ) );
		else
			yeniHucreSayisi = (int) ( ( contentBoyutu.y - gridLayout.padding.vertical ) / ( gridLayout.cellSize.y + gridLayout.spacing.y ) );

		if( yeniHucreSayisi < 1 )
			yeniHucreSayisi = 1;

		if( gridLayout.constraintCount != yeniHucreSayisi )
		{
			gridLayout.constraintCount = yeniHucreSayisi;

			scrollView.m_ContentConstraintCountInit = false;
			scrollView.RefillCells();
		}
	}
}
  • Scroll View/Content‘in RectTransform’unu şu şekilde değiştirin:
  1. Loop Vertical Scroll Rect için (sağdaki scrollbar’ım 20 piksel genişliğinde olduğu için, sağdan [Right] 20 piksel boşluk bıraktım):
  1. Loop Horizontal Scroll Rect için:
  • Scroll View/Content‘e sağ tıklayıp yeni bir UI-Image objesi oluşturun. Oluşan objenin ismini TestItem olarak değiştirin. Oyun içi marketteki entry’leri, TestItem’ın klonları ile UI’da göstereceğiz. TestItem’a tıklayabileceğimiz için, ona öncelikle bir Button component’i ekleyin
  • az önce gördüğümüz Layout Group component’inden fark edeceğiniz üzere, bu plugin UI’ın layout sisteminden faydalanıyor. Bunun için, TestItem’da Layout Element veya Content Size Fitter olmak zorunda. TestItem’a Layout Element component’i ekleyin (Preferred Height, TestItem’ın kaç piksel yüksekliğinde olacağını belirler. Eğer Loop Horizontal Scroll Rect kullanıyorsanız, Preferred Height yerine Preferred Width‘e değer verin, bu durumda TestItem’ın kaç piksel genişliğinde olacağını belirlersiniz):
  • şimdi elimizdeki veriye göre TestItem’ı kişiselleştirmemiz lazım. Hatırlarsanız elimizdeki veri, marketteki eşyaların isim ve ücretlerinden ibaret. Bunun için TestItem’ın içerisinde şu şekilde 2 Text objesi oluşturun (Loop Horizontal Scroll Rect için üste ismi, alta ücreti koyabilirsiniz):
  • TestItem2 adında yeni bir C# script oluşturup içeriğini şu şekilde değiştirin:
using UnityEngine;
using UnityEngine.UI;

public class TestItem2 : LoopScrollRectItem
{
	public Text isimText;
	public Text ucretText;

	public Button buton;
	public Image arkaplan;

	public LayoutElement layoutElement;

	[HideInInspector]
	public bool itemYeniOlusturuldu = true;
}
  • TestItem’ın hangi component’lerini kod ile güncelleyeceksek, o component’ler için birer değişken oluşturduk. Burada dikkat etmemiz gereken en önemli şey, oluşturduğumuz class’ın LoopScrollRectItem sınıfından türemesi. “itemYeniOlusturuldu” değişkeninin ne işe yaradığını bir sonraki script’te göreceksiniz
  • sahnedeki TestItem objesine Test Item 2 component’ini verip değişkenlerin değerlerini doldurun:
  • TestItem objesini Project paneline sürükle-bırak yaparak bir prefab‘a çevirin ve ardından sahneden silin. Oluşturduğunuz prefab’ı Scroll View objesindeki Prefab Source-Prefab değişkenine değer olarak verin (Havuz Ilk Boyutu‘nu olduğu gibi bırakabilirsiniz)
  • LoopScrollRectTest adında yeni bir C# script oluşturun:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class LoopScrollRectTest : MonoBehaviour
{
	// Oyun içi marketteki eşyaların bilgilerini tutan veri türü
	public class ItemBilgileri
	{
		public string isim;
		public int ucret;

		public ItemBilgileri( string isim, int ucret )
		{
			this.isim = isim;
			this.ucret = ucret;
		}
	}

	public LoopScrollRect scrollView;

	// Markette en başta kaç eşya olacağı
	public int ilkItemSayisi = 50;
	// Her Q tuşuna basınca markete kaç yeni eşya geleceği
	public int eklenenItemSayisi = 5;

	// Marketteki her eşyanın yüksekliğinin farklı olup olmayacağı (test amaçlı)
	public bool rastgeleItemYuksekligi = false;

	// Marketteki tüm eşyaların verisi
	private List<ItemBilgileri> itemBilgileri;
	// Hangi index'teki eşyanın seçili olduğu
	private int seciliItemIndex = -1;

	private void Start()
	{
		// Marketteki eşyaların verisini çek (bu örnekte veriyi rastgele oluşturuyoruz)
		itemBilgileri = new List<ItemBilgileri>( ilkItemSayisi );
		for( int i = 0; i < ilkItemSayisi; i++ )
			itemBilgileri.Add( RastgeleItemOlustur() );

		// Scroll View'ı aşağı-yukarı kaydırdıkça, kameranın görüş alanına giren
		// her yeni entry için ItemiGuncelle fonksiyonunu çağır
		scrollView.ItemCallback = ItemiGuncelle;

		// Markette kaç eşya olduğu bilgisini LoopScrollRect'e bildir
		scrollView.totalCount = itemBilgileri.Count;

		// LoopScrollRect'e ekrandaki tüm TestItem'ları yeniden oluşturmasını söyle
		scrollView.RefillCells();
	}

	private void Update()
	{
		// Her Q tuşuna basınca, eklenenItemSayisi kadar yeni eşyayı markete ekle
		if( Input.GetKeyDown( KeyCode.Q ) )
		{
			for( int i = 0; i < eklenenItemSayisi; i++ )
				itemBilgileri.Add( RastgeleItemOlustur() );

			// Markette artık kaç eşya olduğu bilgisini RecyclingListView'e bildir
			scrollView.totalCount = itemBilgileri.Count;

			// Eğer bu esnada Scroll View scroll olmuyor idiyse, scrollbar'ın yüksekliği/genişliği
			// otomatik olarak değişmiyor, Scroll View scroll olunca değişiyor. Scrollbar'ın
			// güncellenmesini garantilemek için, Scroll View'ı kod ile çok cüzi miktarda scroll et
			scrollView.OnScroll( new PointerEventData( EventSystem.current ) { scrollDelta = new Vector2( 0f, 0.001f ) } );
		}

		// W tuşuna basınca, markette seçili bir eşya varsa Scroll View'ı otomatik olarak o eşyaya odakla
		if( Input.GetKeyDown( KeyCode.W ) && seciliItemIndex >= 0 && seciliItemIndex < itemBilgileri.Count )
			scrollView.SrollToCell( seciliItemIndex, 10000f );

		// E tuşuna basınca marketteki tüm eşyaları sil
		if( Input.GetKeyDown( KeyCode.E ) )
		{
			itemBilgileri.Clear();

			// Scroll View'daki tüm TestItem'ları temizle
			scrollView.ClearCells();
		}
	}

	// Market için rastgele eşya verisi oluşturmaya yarar
	private ItemBilgileri RastgeleItemOlustur()
	{
		return new ItemBilgileri( "Item " + itemBilgileri.Count, Random.Range( 1, 1000 ) );
	}

	// index'teki eşyanın TestItem'ı kameranın görüş alanına girdi; TestItem'ın içeriğini
	// o index'teki eşyanın verisini gösterecek şekilde güncelle
	private void ItemiGuncelle( LoopScrollRectItem i, int index )
	{
		TestItem2 item = (TestItem2) i;

		// Isim ve Ucret Text'lerini güncelle
		item.isimText.text = itemBilgileri[index].isim;
		item.ucretText.text = itemBilgileri[index].ucret + " TL";

		// Eğer seçili eşya bu eşya ise, TestItem'ın arkaplan rengini kırmızı yap (if),
		// değilse beyaz yap (else)
		if( index == seciliItemIndex )
			item.arkaplan.color = new Color32( 230, 57, 70, 255 );
		else
			item.arkaplan.color = new Color32( 241, 250, 238, 255 );

		// Eğer bu TestItem için ItemiGuncelle fonksiyonu daha önce çağrılmadıysa
		if( item.itemYeniOlusturuldu )
		{
			// Bu kodu sadece 1 kez çalıştır (aşağıdaki kodu birden çok kez çağırmak boş yere performansı etkiler)
			item.itemYeniOlusturuldu = false;

			// TestItem'a her tıklandığında (onClick) ItemeTiklandi fonksiyonunu çağır
			// Delegate'ler hakkında daha fazla bilgi için: https://yasirkula.com/2019/10/29/unity-c-delegate-ve-eventler/
			item.buton.onClick.AddListener( () => ItemeTiklandi( item ) );

			// Gerekirse TestItem'a rastgele yükseklik ver
			if( rastgeleItemYuksekligi )
				item.layoutElement.preferredHeight = Random.Range( 100f, 200f );
		}
	}

	// Bir TestItem'a tıklandı
	private void ItemeTiklandi( TestItem2 item )
	{
		// Seçili eşya index'ini güncelle. TestItem'a atanan verinin index'ini
		// bulmak için CurrentRow değişkenini kullanmak yeterli
		seciliItemIndex = item.CurrentRow;

		// Kameranın görüş alanındaki tüm TestItem'ları güncelle (hepsi için ItemiGuncelle tekrar çağrılır)
		scrollView.RefreshCells();
	}
}
  • kodda açıklanması gereken her yeri comment’lerle açıklamaya çalıştım. Özetle, oyunun başında LoopScrollRect objesinin ItemCallback‘ine kendi fonksiyonumuzu kaydediyoruz ve akabinde LoopScrollRect’e elimizdeki verinin kaç entry’den oluştuğunu totalCount ile söylüyoruz. Scroll View scroll oldukça, ItemCallback fonksiyonu yeni entry’ler için otomatik olarak çağrılıyor. Elimizdeki entry’lerin sayısı her değiştiğinde, bu değişikliği totalCount ile tekrardan LoopScrollRect’e bildiriyoruz. Elimizdeki tüm verileri temizlediğimizde, ClearCells fonksiyonu ile LoopScrollRect’i de temizliyoruz
  • Loop Scroll Rect Test component’ini sahnenizdeki istediğiniz bir objeye verin. Component’teki Scroll View değişkenine değer olarak sahnedeki Scroll View objesini verin
  • oyunu çalıştırın. Test amaçlı Q tuşu ile markete yeni eşyalar ekleyebilir, W tuşu ile seçili eşyaya odaklanabilir, E tuşu ile de marketteki tüm eşyaları silebilirsiniz

Böylece bu dersin sonuna geldik. Sonraki derslerde görüşmek üzere!

yorum
  1. morpinyo dedi ki:

    Hocam merhaba konuyla alakası yok ama çok aradım konular buldum ama hepsi hata veriyor
    unity de rastgele sayı üretmek istiyorum yani kullanıcı butona tıkladığında rastgele sayı çıkacak örnek 1 den 100 e kadar butona tıklandığında ekranda rastgele sayı çıkacak mesela (3) gibi bunu nasıl yapabilirim rıca etsem örnek kodu yazabilirmisin.

    • yasirkula dedi ki:

      Merhaba, Random.Range kullanabilirsiniz. İçine iki tane int değer girerseniz, 1. int’e büyük eşit, 2. int’ten ise küçük bir rastgele değer döndürür. Sizin örneğinizde, rastgeleSayiText.text = Random.Range(1, 101).ToString(); yapabilirsiniz.

  2. Kağan Parlatan dedi ki:

    Hayat kurtardın çok teşekkürler.

    Oyunu yayınlamadan önce bir baktım scroll rect yavaş çalışıyor, sonra senin yazını buldum ve pooling olarak değiştirdim. 2 gün aldı ama olsun. Artık güzel çalışıyor.

  3. Gamer dedi ki:

    Hocam, yaptığım oyun Game Scene ‘de gayet doğru dürüst duruyor iken telefonda butonlar filan hepsi birbirine girmiş durumda. Nasıl bir yol izleyebilirim?

    • yasirkula dedi ki:

      Öncelikle Canvas Scaler component’ini “Scale With Screen Size” ve “Height” (veya istiyorsanız “Width”) yapmanız lazım. Ardından referans çözünürlük belirledikten sonra UI elemanlarınızı dizebilirsiniz. Ekranın hep sol kenarında olmasını istediğiniz UI elemanlarının anchor X değerlerini 0 yapmanız lazım. Şu dersime bakabilirsiniz: https://yasirkula.com/2015/01/21/unity-ui-arayuz-sistemi/

      Telefon çözünürlüklerini Unity’de test etmek için, Game panelinin altındaki çözünürlük listesine kendi çözünürlüklerinizi ekleyebilirsiniz.

  4. Sıddık Çiçek dedi ki:

    Hocam, “ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
    Parameter name: index” böyle bir hata alıyorum.
    Amacım şu şekilde listede bulunan elemanları sırayla silme işlemi gerçekleştirmek için RemoveAt(); kullanıyorum. Ama oyunda son 2 soru kaldığında bu hatayı veriyor.

    • yasirkula dedi ki:

      Hatayı veren satırı ve onu sarmalayan for loop’unu görmem gerekecek sanırım.

      • Sıddık Çiçek dedi ki:

        for loop dan kastınızı anlamadım

      • yasirkula dedi ki:

        Bu hatayı bir for döngüsünün içerisinde aldığınızı düşündüm. Kabaca kodunuzu görmeden sorunu tespit etmekte güçlük çekiyorum öyle diyeyim.

      • kjhjhjhj dedi ki:

        Hata satırını göstermiyor hocam.

        public void kareleriOlustur()
        {
        for (int i = 0; i < 25; i++)
        {
        GameObject kare = Instantiate(karePrefab, karelerPaneli);

        kare.transform.GetChild(1).GetComponent().sprite = kareSprites[Random.Range(0, kareSprites.Length)];

        kare.transform.GetComponent().onClick.AddListener(() => ButonaBasildi());

        karelerDizisi[i] = kare;
        }

      • yasirkula dedi ki:

        “karelerDizisi” bir array’se boyutunun en az 25 olduğundan emin olun. Bir List ise, belki karelerDizisi.Add kullanmak istemişsinizdir? “kareSprites”ın da boş olmadığından emin olun.

        Aldığınız hata mesajının stacktrace’inin tamamına bakarsanız mutlaka hata satırını gösterir (aşağılarda da olsa gösterir). Ender durumlarda, hata satırı } sembolünün olduğu bir satıra işaret eder. Bu durumda Profiler’dan Deep Profile’ı geçici olarak açarak doğru hata satırını görebilirsiniz.

      • Sıddık Çiçek dedi ki:

        KarelerDizisi kareleri içerisine koyduğum dizi ve bu diziyi 25 elemanlı bir dizi olarak tanımladım. KareSprites değişkeni ile ilgili bir sorun olduğunu düşünmüyorum. Çünkü o değişkene ben kareye basılınca altında resim çıkartsın diye yazdım oraya. stacktrace ‘den kastınız hatanın üzerine tıkladığımda alt tarafta ayrıntısıyla gösterilen yer mi?

      • yasirkula dedi ki:

        Evet stacktrace orası. Hatanın kodunuzda hangi satırdan çıktığı orada mutlaka yazar (bazen biraz aşağılarda olur ama illa yazar). İsterseniz kareSprites.Count’ı da Debug.Log yaparak 0 olmadığından emin olun.

      • Sıddık Çiçek dedi ki:

        Hocam oradan bakıp inceledim. 4 farklı fonksiyonda sıkıntı olduğunu söylüyor. Ama özellikle de “kare.transform.GetComponent().onClick.AddListener(() => ButonaBasildi());” bu satır hata vermekte. Buna anlam veremedim. Ayrıca kareSprites’ın Count ‘unu alamadım anladığım kadarıyla nedeni kareSprites’ın dizi olmasından dolayı ona Length kullandım. Bu şekilde iken Debug’ladım. Bu sefer de sadece 25 defa 5 rakamı yazmakta çünkü 5 farklı sprites kullandım oyunda.

      • yasirkula dedi ki:

        Eğer butona bastığınızda hata alıyorsanız, ButonaBasildi fonksiyonunda hangi satırın hata verdiğine bakmanız lazım. Butona basınca mı hata oluyor?

      • Sıddık Çiçek dedi ki:

        yok hocam butona basılınca hata almıyorum. Onu da kontrol ettim. ButonaBasıldi fonksiyonu çalışıyor.

      • yasirkula dedi ki:

        Bir de kare.transform.childCount’u Debug.Log yapın bakalım. Eğer 0 veya 1 basarsa, GetChild(1) hata veriyordur. Yoksa aklıma artık hiçbir şey gelmiyor.

      • Sıddık Çiçek dedi ki:

        Hocam Debug.Log(kare.transform.childCount); yazınca 2 değeri aldım.

      • yasirkula dedi ki:

        Maalesef bilemiyorum :<

      • Sıddık Çiçek dedi ki:

        Tamamdır hocam Allah razı olsun. Çok yardımcı oldunuz. Gerek yazılarınız ile gerek yorumlar aracılığıyla.

  5. Mehmet dedi ki:

    Hocam merhaba çok aradım bulamadım – sahnede obje kapalı ve o sahneyi unity de başlattığımda objenin aktif olmasını istiyorum bunu nasıl yapabilirim

  6. hepaldim dedi ki:

    Hocam kusura bakmayın alakasız biraz ama ben Sizin Admob icin yazdığınız singelton scripti kullanıyorum. Sorum, Farklı scenelerde bannerın yerini nasıl ayarlayabilriz? Sizin yazdığınızda sadece bir tane secebiliyor gibiyiz?

    • yasirkula dedi ki:

      instance’ı public yapıp ReklamScript.instance.bannerPozisyonu değişkenini değiştirdikten sonra, BannerReklamAl fonksiyonunu çağırmanız lazım.

      • hepaldim dedi ki:

        Hocam Cok özür dileyerek anlamadığımı belirtmek isterim 😦

        public static ReklamScript instance = null; 1.si bu heralde;

        diğerlerini anlamadım ama.

      • yasirkula dedi ki:

        ReklamScript.instance.bannerPozisyonu = blabla;
        ReklamScript.BannerReklamAl();

  7. Cylon dedi ki:

    Merhaba, peki bir şey sorabilir miyim? Farklı yüksekliklere sahip objelerle nasıl scroll oluşturabiliriz? Mesela konuşma baloncuğu mantığında bir şey… İlk baloncukta 1 cümle vardır yüksekliği 1 birimdir, İkincisinde -bir altında- 2 cümle vardır yüksekliği 2 birimdir gibi bir şey. Grid Layout, Horizontal Layout ve Vertical Layout componentlerinde bu işe yaramıyor.

    • yasirkula dedi ki:

      LoopScrollRect plugin’i bunu destekliyor. LayoutElement’in Preferred Height değerini, ItemiGuncelle fonksiyonunda kodla değiştirebilirsiniz.

yasirkula için bir cevap yazın Cevabı iptal et

Bu site, istenmeyenleri azaltmak için Akismet kullanıyor. Yorum verilerinizin nasıl işlendiği hakkında daha fazla bilgi edinin.