SON GÜNCELLEME: 23 Eylül 2020 (Diğer Optimizasyonlar’a bir yeni optimizasyon eklendi)
Yine ve yeniden merhabalar,
Bu yazıda, kimilerini kendim tecrübe ettiğim, kimilerini de ordan burdan derlediğim Unity 3D optimizasyon tekniklerinden bahsedeceğim. Yeni şeyler öğrendikçe bu yazıyı güncellemeye çalışacağım.
Optimizasyon çok ucu açık bir şey olduğu için, kimsenin “optimizasyon konusuna hakimim” gibi bir söylemde bulunabileceğini sanmıyorum. Tam olarak da bu yüzdendir ki, kendi bildiğiniz optimizasyon tekniklerini de bu yazı altında yorum olarak paylaşırsanız, burada Türk oyun geliştiricileri için faydalı bir kaynak oluşturabiliriz diye ümit ediyorum.
Genel Kural: Oyununuzda sistemi en çok hangi bileşen(ler)in yorduğunu görmek için her daim Profiler kullanın. Profiler dersim için: https://yasirkula.com/2020/03/26/unity-profiler-kullanimi/
Yazıdaki optimizasyon tekniklerinin bazılarının başında [1], [2] gibi referanslar bulunmakta; bu referanslara yazının sonundaki “Yararlanılan Kaynaklar” kısmından ulaşarak, o optimizasyon tekniklerinin bahsi geçtiği orijinal dokümanları inceleyebilirsiniz.
Bazı optimizasyon tekniklerinde Garbage Collector (GC) terimini kullandım, bundan kısaca bahsetmek istiyorum. C++’ın aksine C#’ta, artık kullanılmayan hafıza otomatik olarak boşaltılır (automatic memory management). Bu işlemi Garbage Collector gerçekleştirir. Kullanılmayan hafızanın temizlenmesi önemlidir çünkü aksi taktirde bu hafıza tekrar kullanılamaz ve oyununuzun kullandığı RAM gitgide artar, ta ki oyun artık tüm RAM’i harcayıp çökene kadar. Garbage Collector çalıştığında, oyunun kullandığı tüm RAM’in üzerinden geçer ve RAM’in artık kullanılmayan kısımlarını bularak buraları temizler. Bu işlem biraz zaman alır ve bu esnada oyun donar (bu donma süresi, oyunun kullandığı RAM miktarı ile doğru orantılıdır); bu yüzden de garbage collector’ın olabildiğince az çalışması elzemdir. Peki garbage collector ne zaman çalışır? Oyuna atanmış olan RAM ağzına kadar doluyken, yeni bir obje için (Instantiate edilmiş bir obje, yeni bir array vs.) biraz daha hafıza gerektiğinde. İşte bu durumda garbage collector çalışır, kullanılmayan hafıza temizlenir ve eğer yeni oluşturulan objenin sığabileceği kadar yer temizlendiyse obje hafızanın o kısmını kullanmaya başlar. Yeteri kadar yer temizlenemediyse mecburen oyunun kullandığı RAM miktarı artar; bu da sonraki garbage collector operasyonlarının daha uzun sürmesi anlamına gelir. Tabi oyun RAM’i zaten tamamen kullanıyor idiyse o zaman maalesef oyun çöker (devasa bir oyun yapmıyorsanız buna çok kafanızı takmayın, bu sorun benim hiç başıma gelmedi). Özet geçecek olursak:
- Instantiate ile veya new takısı ile oluşturduğunuz objeler, array’ler vs. hafızada yer kaplar, eğer hafıza doluysa garbage collector çalışır. Buradaki new olayı aslında biraz daha karmaşık bir konu çünkü
new int[]ile oluşturulan bir array garbage collector’ı çalıştırırken,new Vector3ile oluşturulan bir vektör çalıştırmaz çünkü Vector3’ün kullandığı hafıza, garbage collector’ın çalıştığı heap‘te değil, ayrı bir hafıza olan stack‘te tutulur (dediğim gibi bu biraz karmaşık bir konu, siz sadece Unity’de çokça kullandığımız new Vector3‘ün herhangi bir sıkıntı olmadığını ve garbage collector’a bir etkisi olmadığını bilin yeter). - Garbage collector çalışırken oyun tamamen donar, bu süre oyunun kullandığı RAM miktarı ile doğru orantılı olarak artar.
Profiler penceresinin GC Alloc sütunu, o frame’de ne kadar yeni hafıza kullanıldığını gösterir. Özellikle mobil oyunlarda, bu değer oyunun büyük çoğunluğunda 0 byte olmalıdır. Eğer her frame’de belli bir miktar hafıza kullanılıyorsa, garbage collector’ın olabildiğince az çalışması için, hafızayı kullanan kodu Profiler ile tespit edin ve o kodu 0 byte‘a düşürecek şekilde optimize edin (diyelim sürekli yeni bir array oluşturuluyorsa, onun yerine bu array’i oyunun başında bir kere oluşturup tekrar tekrar kullanın).
Script Optimizasyonu
- [16] Scriptlerinizde içi boş Unity fonksiyonları bırakmayın (özellike Update, LateUpdate ve FixedUpdate). Bu fonksiyonlar Unity Script Reference‘de MonoBehaviour altında Messages olarak listelenir ve scriptlerinizde bu fonksiyonlardan herhangi birisi varsa, o fonksiyon içi boş olsa bile çağrılır. Update, LateUpdate ve FixedUpdate fonksiyonları oyun sırasında defalarca kez çağrıldığı için de özellikle bu fonksiyonlar konusunda özen gösterin: fonksiyonu kullanmıyor musunuz? O zaman kodunuzdan silin.
- Kodunuzda bir component’e birden çok kez erişiyorsanız, o component’e her seferinde GetComponent ile erişmek yerine onu bir değişkenin içinde tutun ve sonraki seferlerde bu değişkeni kullanın.
Optimizasyon öncesi:
void FixedUpdate()
{
// Yukarı yönde güç uygula
GetComponent<Rigidbody>().AddForce( Vector3.up );
}
public void MuzikCal()
{
GetComponent<AudioSource>().Play();
}
Optimizasyon sonrası:
private Rigidbody cachedRigidbody;
private AudioSource cachedAudioSource;
void Awake()
{
cachedRigidbody = GetComponent<Rigidbody>();
cachedAudioSource = GetComponent<AudioSource>();
}
void FixedUpdate()
{
// Yukarı yönde güç uygula
cachedRigidbody.AddForce( Vector3.up );
}
public void MuzikCal()
{
cachedAudioSource.Play();
}
- [15] GameObject.Find ile sahnedeki bir objeye erişmek yavaş bir işlemdir. Bunun daha optimize versiyonu GameObject.FindWithTag‘dır. Ancak daha daha iyi bir optimizasyon, ilgili objeyi bir değişkende tutmaktır. Özellikle Object.FindObjectOfType kullanıyorsanız bu optimizasyon çok daha önem arz etmektedir.
Optimizasyon öncesi:
void Update()
{
// Bu objeyi, Obje2'nin olduğu yere ışınla
transform.position = GameObject.Find( "Obje2" ).transform.position;
}
void Obje3uDeaktifEt()
{
GameObject.FindWithTag( "Obje3unTagi" ).SetActive( false );
}
Optimizasyon sonrası:
private Transform obje2ninTransformu;
private GameObject obje3;
void Awake()
{
obje2ninTransformu = GameObject.Find( "Obje2" ).transform;
obje3 = GameObject.FindWithTag( "Obje3unTagi" );
}
void Update()
{
// Bu objeyi, Obje2'nin olduğu yere ışınla
transform.position = obje2ninTransformu.position;
}
void Obje3uDeaktifEt()
{
obje3.SetActive( false );
}
NOT: Camera.main değişkeni, sahnedeki kameraya kolayca erişebilmenize olanak sağlar ama arkaplanda GameObject.FindWithTag("Main Camera") fonksiyonunu çağırır. Yani Camera.main’e sıklıkla erişiyorsanız, sonucu bir değişkene atıp o değişkeni kullanın.
- GetComponents veya GetComponentsInChildren fonksiyonları, belli bir türdeki tüm component’leri bulup bir array olarak döndürürler. Her yeni array, garbage collector için biraz daha iş demektir. Bunun yerine, bu fonksiyonların List parametre alan versiyonlarını kullanın. Bu şekilde, bulunan sonuçlar halihazırda var olan List’te depolanır, yeni bir array oluşmamış olur.
Optimizasyon öncesi:
void ColliderlariKapat()
{
Collider[] colliderlar = GetComponents<Collider>();
for( int i = 0; i < colliderlar.Length; i++ )
colliderlar[i].enabled = false;
}
Optimizasyon sonrası:
private readonly List<Collider> colliderlar = new List<Collider>();
void ColliderlariKapat()
{
GetComponents<Collider>( colliderlar );
for( int i = 0; i < colliderlar.Count; i++ )
colliderlar[i].enabled = false;
}
- Bazen oyun boyunca bir objeyi sürekli Instantiate ve Destroy etmeniz gerekebilir (örneğin bir FPS oyunundaki mermi prefabı veya bir patlama partikül efekti). Ancak bu Instantiate ve Destroy komutlarının çok fazla kullanımı performans açısından iyi bir şey değil. Tam olarak da böyle durumlar için pooling pattern dediğimiz bir teknik geliştirilmiş. Bunun çalışma prensibi basit: sürekli kullandığınız bir prefab’ın klonunu Destroy etmek yerine deactivate edip pool’a (havuz) atıyorsunuz. Daha sonradan bu prefab’ın başka bir klonuna ihtiyaç duyduğunuzda, önce havuzda hazırda bir klon var mı diye bakıyorsunuz; varsa havuzdaki klonu kullanıyorsunuz. Havuzda klon yoksa ancak o zaman yeni bir klon Instantiate ediyorsunuz. Bu basit teknik, FPS oyunlarından tutun infinite runner oyunlarına kadar pek çok oyun türünde kullanılmaya müsaittir. Daha fazla detay için şu derse göz atabilirsiniz: https://yasirkula.com/2019/10/30/unity-pooling-obje-havuzu-patterni/
- Oyununuzun başında Application.targetFrameRate‘e bir değer verin. Bu değişken, oyunun hedeflediği FPS değerini belirler. Diyelim değerini 60 yaparsanız, oyun 60 FPS’in üzerine çıkmaz. Oyunun 120 FPS’te çalıştırılmasına göre 60 FPS’te çalıştırılması, özellikle mobil cihazlarda bataryayı daha yavaş harcamaya, cihazın daha yavaş ısınmasına ve CPU’nun daha az yorulmasına yardımcı olur. Mobil cihazlarda targetFrameRate’i mümkünse 30, oyununuz göze çok akıcı gelmiyorsa da 60 yapın. Daha da yüksek bir rakamın size bir faydası olmayacaktır.
- Vector struct’larında (mesela Vector3) Distance adında bir fonksiyon ve magnitude adında bir değişken bulunmakta. Bu iki komut da esasında bir vektörün uzunluğunu ölçmeye yarıyor. Ancak hesaplama sırasında karekök kullanmak gerektiğinden aslında bu işlem sanıldığından daha zahmetli bir şey. Eğer ki bu uzunluk değerini direkt kullanıcıya göstermeyecekseniz (örneğin iki vektörün uzunluğunu kıyaslamaya çalışıyorsanız), o zaman daha hızlı çalışan sqrMagnitude değişkenini kullanmayı düşünebilirsiniz. Bu değişken, bir vektörün uzunluğunun karesini döndürür ve bu değeri hesaplamak magnitude’a (ve Distance’a) göre daha hızlıdır.
Optimizasyon öncesi:
public float maksimumMesafe = 5f;
Vector3 vektor1, vektor2, vektor3;
void BirFonksiyon()
{
// vektor1 ve vektor2 arasındaki mesafe maksimumMesafe'den küçükse
if( Vector3.Distance( vektor1, vektor2 ) <= maksimumMesafe )
{
// bla bla
}
// vektor3'ün uzunluğu 8.0'dan büyükse
if( vektor3.magnitude > 8f )
{
// bla bla
}
}
Optimizasyon sonrası:
public float maksimumMesafe = 5f;
private float maksimumMesafeKaresi;
Vector3 vektor1, vektor2, vektor3;
void Start()
{
maksimumMesafeKaresi = maksimumMesafe * maksimumMesafe;
}
void BirFonksiyon()
{
// vektor1 ve vektor2 arasındaki mesafe maksimumMesafe'den küçükse
if( ( vektor1 - vektor2 ).sqrMagnitude <= maksimumMesafeKaresi )
{
// bla bla
}
// vektor3'ün uzunluğu 8.0'dan büyükse
if( vektor3.sqrMagnitude > 64f )
{
// bla bla
}
}
- Eğer kodlarınızda bir sayıyı veya değişkeni sürekli belli bir sayıya bölüyorsanız; o zaman bu bölme işlemini bir çarpma işlemi ile değiştirin çünkü çarpma işlemi bölme işlemine göre çok daha hızlı hesaplanır.
Optimizasyon öncesi:
public float surtunme = 2f;
void Update()
{
// Objeyi ileri yönde hareket ettir
transform.Translate( Vector3.forward * Time.deltaTime / surtunme );
}
Optimizasyon sonrası:
public float surtunme = 2f;
private float _1BoluSurtunme;
void Start()
{
_1BoluSurtunme = 1f / surtunme;
}
void Update()
{
// Objeyi ileri yönde hareket ettir
transform.Translate( Vector3.forward * Time.deltaTime * _1BoluSurtunme );
}
- [15] SendMessage/BroadcastMessage fonksiyonları performans için hiç iyi değildir ve bu yüzden kullanılmamalıdır. Onun yerine, çağırmak istediğiniz fonksiyon hangi objedeyse o objeyi bir değişkende tutup, bu değişken vasıtasıyla fonksiyonu direkt olarak çağırmak “çok” daha hızlıdır.
- foreach fonksiyonu, bir dizi objenin üzerinden hızlıca geçmek için ideal durabilir (başta ben de çok kullanıyordum). Ancak işin aslı, her foreach kullanımında arkaplanda bir IEnumerator objesi oluşturulur ve bu da Garbage Collector‘a fazladan yük anlamına gelir. Bu yüzden foreach kullanmak yerine, sadece bir satır fazla kod yazarak normal for kullanın.
Optimizasyon öncesi:
public Transform[] hareketEttirilecekObjeler;
void Update()
{
foreach( Transform obje in hareketEttirilecekObjeler )
{
obje.Translate( Vector3.forward * Time.deltaTime );
}
}
Optimizasyon sonrası:
public Transform[] hareketEttirilecekObjeler;
void Update()
{
for( int i = 0; i < hareketEttirilecekObjeler.Length; i++ )
{
Transform obje = hareketEttirilecekObjeler[i];
obje.Translate( Vector3.forward * Time.deltaTime );
}
}
- Eğer build alacağınız platform Mono yerine IL2CPP destekliyorsa (Player Settings‘teki Scripting Backend ayarından bunu öğrenebilirsiniz), IL2CPP ile build almayı deneyin. Mono’da alınan build’lar, script’leri intermediate language (IL) denilen formata çevirirken, IL2CPP’de alınan build’lar ise kodu bu IL formatından C++ (CPP) formatına çevirir (farkettiyseniz, IL2CPP’nin açılımı da “intermediate language to C++“tır). Genel olarak bu bedavadan performans artışı anlamına gelir. Android’de IL2CPP ile build alabilmek için NDK kurulu olması gereklidir, eğer kurulu değilse Unity sizi download linkine yönlendirecektir.
- [5][14]
public int Degisken { get; set; }şeklinde tanımlanmış property‘leripublic int Degisken;şeklinde normal değişkenlere çevirin. Eğer değişkenin Inspector’da gözükmesini istemiyorsanız, başına[System.NonSerialized]attribute‘ü ekleyin. - [14] Bir fonksiyonda sürekli yeni bir List veya array oluşturuyorsanız, onun yerine bu List veya array’i oyunun başında bir kere oluşturup tekrar tekrar kullanın. Bu optimizasyon özellikle garbage collector‘ı çok sevindirecektir.
- [14] Dokunmatik ekranlar için kod yazarken Input.touches kullanıyorsanız, onun yerine Input.touchCount ve Input.GetTouch kullanın. Dokunmatik ekran dersim için: https://yasirkula.com/2013/07/17/unity-ile-androide-uygulama-gelistirmek-1-dokunmatik-ekran-entegrasyonu/
Optimizasyon öncesi:
for( int i = 0; i < Input.touches.Length; i++ )
{
Touch parmak = Input.touches[i];
}
Optimizasyon sonrası:
for( int i = 0; i < Input.touchCount; i++ )
{
Touch parmak = Input.GetTouch( i );
}
- [14] OnCollisionEnter vs. fonksiyonlara parametre olarak gelen Collision‘ın contacts değişkenini her kullandığınızda yeni bir array oluşturulur. Onun yerine, eğer Unity 2018.3 veya sonrasını kullanıyorsanız, contactCount ve GetContact kullanın (tıpkı Input.touches optimizasyonu gibi).
- Bir objenin tag‘inin belli bir değere eşit olup olmadığına bakarken
obje.tag == "Player"kodu yerineobje.CompareTag("Player")kodunu kullanın. - [15] Kodunuzun belli bir noktasında, pek çok string’i birleştirerek yeni bir string oluşturmanız gerekiyorsa, string’leri + ile birbirine eklemek yerine StringBuilder kullanın. Sadece iki string’i birleştirecekseniz + kullanmaya devam edin, üç veya dört string’i birleştirecekseniz string.Concat fonksiyonunu kullanın, daha fazlası için StringBuilder kullanın. Hatta eğer mümkünse her seferinde yeni bir StringBuilder objesi oluşturmak yerine, tek bir StringBuilder objesi oluşturup onu tekrar tekrar kullanın. Her kullanımdan sonra, StringBuilder.Length‘i 0’a eşitleyerek StringBuilder’ın içini boşaltın.
Optimizasyon öncesi:
public void StringBirlestirme( int skor, int para )
{
string s1 = "Skor: " + skor;
string s2 = "Para: " + para + "TL";
string s3 = "Skor " + skor + " ve para " + para + "TL";
}
Optimizasyon sonrası:
// StringBuilder, kapasitesi string'i depolamaya yetmeyince
// otomatik olarak büyür ama dilersek StringBuilder'ın ilk kapasitesini
// objeyi oluştururken elle belirleyebiliriz (bu örnekte 100)
private readonly StringBuilder sb = new StringBuilder( 100 );
public void StringBirlestirme( int skor, int para )
{
string s1 = "Skor: " + skor;
string s2 = string.Concat( "Para: ", para, "TL" );
string s3 = sb.Append( "Skor " ).Append( skor ).Append( " ve para " ).Append( para ).Append( "TL" ).ToString();
sb.Length = 0; // Sonraki kullanımlar için StringBuilder'ın içini boşalt
}
- [15] String.StartsWith veya String.EndsWith fonksiyonlarının tek bir string parametre alan versiyonlarını kullanıyorsanız, onlar yerine şu fonksiyonları kullanın:
// "asd".StartsWith( "a" ) yerine PerformansliStartsWith( "asd", "a" ) kullanın
bool PerformansliStartsWith( string a, string b )
{
int aLen = a.Length;
int bLen = b.Length;
int ap = 0; int bp = 0;
while( ap < aLen && bp < bLen && a[ap] == b[bp] )
{
ap++;
bp++;
}
return ( bp == bLen );
}
// "asd".EndsWith( "a" ) yerine PerformansliEndsWith( "asd", "a" ) kullanın
bool PerformansliEndsWith( string a, string b )
{
int ap = a.Length - 1;
int bp = b.Length - 1;
while( ap >= 0 && bp >= 0 && a[ap] == b[bp] )
{
ap--;
bp--;
}
return ( bp < 0 );
}
- TextMesh Pro objelerinizde bir int, float vb. değişkenin değerini gösterecekseniz, bunu garbage collector’u yormadan yapın: https://yasirkula.com/2020/09/13/unity-cerez-ders-textmesh-pro-yazilarinda-sayi-gostermenin-efektif-yolu/
- [15] Bu maddeyi direkt örnek üzerinden göstereceğim.
Optimizasyon öncesi:
void Update()
{
for( int i = 0; i < array.Length; i++ )
{
if( boolDegisken )
BirFonksiyon( array[i] );
}
}
Optimizasyon sonrası:
void Update()
{
if( boolDegisken )
{
for( int i = 0; i < array.Length; i++ )
{
BirFonksiyon( array[i] );
}
}
}
- [15] Update‘te çağırdığınız bir fonksiyonu, her frame’de çalıştırmak yerine 3-4 frame’de bir veya her 0.1 saniyede bir çalıştırmanız kafiyse (kullanıcı aradaki farkı anlamayacaksa), o fonksiyonu boş yere her frame’de çalıştırmayın.
Optimizasyon öncesi:
void Update()
{
PahaliBirFonksiyon();
}
Optimizasyon sonrası:
void Update()
{
if( Time.frameCount % 3 == 0 ) // Her 3 frame'de bir çalışır
PahaliBirFonksiyon();
}
// VEYA
private float fonksiyonCagrilmaAni;
void Update()
{
if( Time.time >= fonksiyonCagrilmaAni )
{
PahaliBirFonksiyon();
fonksiyonCagrilmaAni = Time.time + 0.1f; // Her 0.1 saniyede bir çalışır
}
}
- [15] Bir kodun sadece obje kameranın görüş alanı içindeyse çalışmasını istiyorsanız, o kodu rendererComponenti.isVisible true ise çalıştırabilirsiniz. Görüş alanından kasıt, objenin kameranın field of view‘ının içinde olmasıdır, yani objenin bir duvarın arkasında gizleniyor olması isVisible’ın değerine etki etmez.
- [14][16] LINQ kullanmayın. LINQ performans olarak hiç iyi değildir. Gerekirse biraz daha fazla kod yazın ama LINQ’ten kaçının.
- [14] Regular Expression‘ları mümkünse kullanmayın, illa kullanmanız gerekiyorsa da bu regular expression’ı
new Regexile oluşturup bir değişkende tutun ve regular expression’ın sağlanıp sağlanmadığınaregexDegisken.Matchfonksiyonuyla bakın; yani static olanRegex.Matchfonksiyonunu kullanmayın. - [8] transform.SetParent fonksiyonunu gereksiz kullanmayın; bu fonksiyonun sık kullanımı kolaylıkla performansı düşürebilir. Bu yüzdendir ki Instantiate fonksiyonunun
Transform parentparametre alan versiyonları bulunmaktadır: eğer bir objeyi Instantiate edip hemen ardından SetParent ile parent’ını değiştiriyorsanız, bu parent belirleme kısmını direkt Instantiate’in içinde yaparak performansı biraz daha iyileştirebilirsiniz. Eğer object pooling tekniğinden faydalanıyorsanız da, objeleri havuza eklerken mümkünse SetParent kullanmayın. - transform.position ve transform.eulerAngles yerine mümkünse transform.localPosition ve transform.localEulerAngles kullanın. İlk saydıklarım objenin global konum ve rotasyonunu döndürürken, ikinci saydıklarım ise objenin local konum ve rotasyonunu döndürürler (Inspector’da Position ve Rotation olarak gözüken değerler aslında ikinci saydığım değerlerdir). position ve eulerAngles değerleri, objenin parent‘larının Position, Rotation ve Scale değerleriyle beraber değiştiği için, position ve eulerAngles’ı hesaplarken parent’ların Transform’larının da hesaba dahil olması gerekir; bu hesaplama hiçbir zaman localPosition ve localEulerAngles kadar hızlı değildir. Eğer position ve eulerAngles’ına erişmek istediğiniz objenin bir parent’ı olmadığını veya tüm parent’larının (parent’ının parent’ı vs.) 0,0,0 Position, Rotation ve Scale değerlerine sahip olduğunu kesin olarak biliyorsanız, o zaman localPosition ve localEulerAngles kullanın.
- [15] Bir önceki maddede değindiğim gibi, transform.position ve transform.eulerAngles‘ı hesaplamak nispeten yavaştır. Bu yüzden bu değerlere mümkün olduğunca az erişmeye çalışın (örneğe bakınca anlayacaksınız).
Optimizasyon öncesi:
private void Update()
{
// Objeyi daima Y=10 koordinatında tut
// transform.position'a 2 kere erişiyoruz: x ve z koordinatlarını alırken
transform.position = new Vector3( transform.position.x, 10f, transform.position.z );
}
Optimizasyon sonrası:
private void Update()
{
// Bu sefer transform.position'a sadece bir kere erişiyoruz
Vector3 konum = transform.position;
konum.y = 10f;
transform.position = konum;
}
- [3] Hem transform.position‘a hem de transform.rotation‘a (veya transform.eulerAngles‘a) aynı anda değer veriyorsanız, bunun için transform.SetPositionAndRotation fonksiyonunu kullanın.
Optimizasyon öncesi:
private void Update()
{
obje1.transform.position = obje2.transform.position;
obje1.transform.eulerAngles = new Vector3( 0f, 90f, 0f );
}
Optimizasyon sonrası:
private void Update()
{
obje1.transform.SetPositionAndRotation( obje2.transform.position, Quaternion.Euler( new Vector3( 0f, 90f, 0f ) ) );
}
- [3] Bir kodu sadece objenin Transform‘u değiştiyse çalıştırmak istiyorsanız (örneğin konumu değiştiyse), kodu o Transform’un hasChanged değeri true olduğunda çalıştırın. Bir Transform’un herhangi değeri değiştiğinde, o Transform’un hasChanged’i otomatik olarak true olur. Ancak hasChanged true ise kodunuzu çalıştırdıktan sonra, hasChanged’i elle false yapın çünkü Unity bu değeri kendisi false yapmaz.
- [12][14][16] Oyununuzun bazı parametrelerini bir JSON veya XML dosyasından çekiyorsanız, onun yerine ScriptableObject kullanmayı düşünebilirsiniz. ScriptableObject’in dezavantajı, içinde saklanan değerlerin oyunu build aldıktan sonra değiştirilememesidir ancak bu sizin için bir sıkıntı değilse, ScriptableObject kullanarak daha performanslı bir şekilde parametreleri okuyabilirsiniz. Kullanıcının parametreleri değiştirebilmesini istediğiniz için özellikle JSON kullanıyorsanız, bu JSON’u parse etmek için mümkünse Unity’nin JsonUtility sınıfını kullanın. JsonUtility dersim için: https://yasirkula.com/2020/04/03/unity-jsonutility-kullanimi/
- Mobil oyunlarınızda, script’lerinizde OnMouseDown gibi fonksiyonlar bırakmayın. Bu fonksiyonlar varken build alırsanız Unity konsola bir uyarı basarak bunun mobilde performansı düşürebileceğinden yakınacaktır. İlaveten, Input.GetMouseButtonDown gibi içerisinde Mouse kelimesi geçen bir Input fonksiyonu veya değişkeni kullanmıyorsanız, oyunun başında
Input.simulateMouseWithTouches = false;yapın. - [14] Materyallerin SetColor veya SetTexture gibi fonksiyonları ilk parametre olarak bir string veya int kabul eder. Eğer bu fonksiyonları birden çok kez çağırıyorsanız, performans için int’li fonksiyonları tercih edin. “_Color” gibi herhangi bir string parametrenin int karşılığını öğrenmek için, Shader.PropertyToID fonksiyonunu kullanabilirsiniz. Bu fonksiyon aynı string değeri için daima aynı int değeri döndürür, o yüzden bu fonksiyonun döndürdüğü int’i oyunun başında bir değişkende tutun ki birden fazla kez bu fonksiyonu çağırmak zorunda kalmayın.
- [14] Yukarıda bahsettiğim durum Animator için de geçerli. Animator’ın SetFloat veya SetBool gibi fonksiyonlarını da int parametre ile çağırın. Bu int’in değerini öğrenmek için ise, Animator.StringToHash fonksiyonunu kullanın.
- Kodlarınızda kullandığınız Debug.Log‘ların oyunu build aldığınızda da çalışmaya devam ettiğini biliyor muydunuz? Android’de bu log’lara çeşitli yollarla bakabilirken iOS’ta ise Xcode’un konsolu ile bakabilirsiniz. Debug.Log’lar, özellikle sık kullanılması halinde (mesela Update‘te), oyunun performansına gözle görülür derecede olumsuz etki edebilirler. Bu durumda yapabileceğiniz üç alternatif var:
- Sadece editörde gözükmesini istediğiniz Debug.Log’ları
#if UNITY_EDITORve#endifsatırlarıyla çevreleyin:
#if UNITY_EDITOR
Debug.Log( "Bu log sadece editörde gözükecek" );
#endif
- [4][16] Hiçbir Debug.Log’un build alınan oyunda gözükmesini istemiyorsanız, şöyle bir Debug2 class’ı oluşturun:
using System.Diagnostics;
using Debug = UnityEngine.Debug;
public static class Debug2
{
[Conditional( "UNITY_EDITOR" )] public static void Log( object message ) { Debug.Log( message ); }
[Conditional( "UNITY_EDITOR" )] public static void LogWarning( object message ) { Debug.LogWarning( message ); }
[Conditional( "UNITY_EDITOR" )] public static void LogError( object message ) { Debug.LogError( message ); }
[Conditional( "UNITY_EDITOR" )] public static void LogException( System.Exception exception ) { Debug.LogException( exception ); }
}
Ardından artık Debug.Log yerine Debug2.Log fonksiyonunu kullanın. Bu fonksiyon System.Diagnostics.Conditional("UNITY_EDITOR") attribute’üne sahiptir, yani UNITY_EDITOR condition’ının sağlanmadığı durumlarda (build alırken), Debug2.Log fonksiyonları script’lerinizden otomatik olarak silinir.
- Debug.Log’ların oyunda gözükmesini istiyor ama stacktrace‘lerinin (o log’un olduğu fonksiyonu hangi fonksiyonların çağırdığını gösteren bilgi) gözükmesini istemiyorsanız, Player Settings‘te Logging‘in altında yer alan değerleri ScriptOnly‘den None‘a çekin. Yalnız bu durum editörde aldığınız log’ları da etkiler, o yüzden bu değişikliği sadece build alırken yapıp, build aldıktan sonra değişikliği geri almak isteyebilirsiniz. Burada işin güzel yanı, normal log’ların stacktrace’lerini gizleyip sadece Debug.LogError‘ların veya hata mesajlarının (Debug.LogException) stacktrace’lerinin gözükmesini sağlayabilirsiniz (hataların kaynağını daha kolay tespit edebilmek için). Bunun için Logging ayarlarında Error ve Exception‘ı ScriptOnly’de bırakmanız yeterli.
- [1] Bir MeshRenderer veya SkinnedMeshRenderer objenin materyaline erişmek için iki değişken vardır: material ve sharedMaterial. İlki, objenin mevcut materyalini klonlar, bu klon materyali objeye atar ve ardından bu materyali döndürür; ikincisi ise objenin materyalini klonlamadan direkt döndürür. Her yeni materyal performans için kötü olduğu için, mümkün olduğunca material‘den kaçının. Örneğin objenin materyalini herhangi bir şekilde değiştirmeyecek ama sadece materyalin rengini vs. kontrol edeceksiniz daima sharedMaterial kullanın. Yok ama sahnedeki sadece belli bir objenin rengini değiştirecekseniz, o zaman material kullanabilirsiniz. Eğer materyalin alabileceği renkler belliyse (mesela ya kırmızı ya da beyaz), bunun daha iyi bir yolu, materyalin rengini değiştirmek yerine direkt materyali değiştirmektir. Yani elinizde aynı materyalin hem beyaz hem kırmızı versiyonu olur ve objenin direkt sharedMaterial‘inin değerini değiştirerek objenin rengini değiştirirsiniz. Maalesef sharedMaterial ile ilgili dikkat etmeniz gereken önemli bir husus var: editördeyken objenin sharedMaterial’ine yapılan değişiklikler, direkt Project’teki materyal asset’ini değiştirir, yani yaptığınız değişiklik Play moddan çıkınca geri alınmaz.
- [6] Vektör hesaplaması yaparken işlem önceliğine dikkat ederek bedavadan optimizasyon yapabilirsiniz:
// İki (Vector3*float) hesaplaması var: YAVAŞ
Vector3 v1 = Vector3.forward * 5f * Time.deltaTime;
// Bir (float*float) ve bir (Vector3*float) hesaplaması var: DAHA HIZLI
Vector3 v2 = Vector3.forward * ( 5f * Time.deltaTime );
Vector3 v3 = 5f * Time.deltaTime * Vector3.forward;
- [6] Bir List‘in elemanlarının üzerinden for döngüsü ile geçiyorsanız ve bu List’in eleman sayısı for’dayken değişmeyecekse, List.Count‘u sadece bir kere çağırmaya çalışın:
Optimizasyon öncesi:
float ListElemanlariniTopla( List<float> list )
{
float sonuc = 0;
for( int i = 0; i < list.Count; i++ )
sonuc += list[i];
return sonuc;
}
Optimizasyon sonrası:
float ListElemanlariniTopla( List<float> list )
{
float sonuc = 0;
for( int i = 0, elemanSayisi = list.Count; i < elemanSayisi; i++ )
sonuc += list[i];
// Veya list'i baştan sona değil sondan başa gez, Count yine bir kere kullanılır
//for( int i = list.Count - 1; i >= 0; i-- )
// sonuc += list[i];
return sonuc;
}
- [15] StartCoroutine ile bir coroutine‘i başlatırken mutlaka bir miktar RAM kullanılır (garbage collector için kötü), gereksiz coroutine’lerden kaçının.
- [15] Bir coroutine‘i bir frame bekletmek için
yield return 0;kullanmayın, onun yerineyield return null;kullanın. İlk kod, boxing’den dolayı RAM harcar. - [15] Coroutine‘lerde kullanılan WaitForSeconds‘ı birden çok kez kullanabilirsiniz, her seferinde
new WaitForSecondsile yeni bir obje oluşturmanıza gerek yok. - [4] Kodunuzda 2 boyutlu array kullanıyorsanız, bu array’leri
int[,]şeklinde değil deint[][]şeklinde tanımlayın. - [4] İnternetten veri indirmek için Unity’nin WWW sınıfını kullanıyorsanız, onun yerine UnityWebRequest kullanın. WWW her seferinde yeni bir thread oluştururken, UnityWebRequest’in kendi içinde bir thread havuzu bulunmaktadır.
- [7] Eğer oyununuzu Profiler ile debug ettiğinizde, oyundaki takılmaların büyük bir kısmının garbage collector (GC) tarafından kaynaklandığını tespit ederseniz, Player Settings‘teki Use incremental GC seçeneğini (Unity 2019.1+) açıp oyunu bir de öyle test edin.
- [5] Dictionary ve HashSet veri türlerinin tüm elemanlarının üzerinden foreach ile geçmek nispeten yavaştır. Eğer bu veri türlerinin sağladığı kolaylıklara özelliklere ihtiyacınız yoksa List kullanın veya aynı veriyi hem List hem de Dictionary/HashSet’te tutup tüm elemanların üzerinden geçerken List’i kullanın.
- [5] Key‘i UnityEngine.Object türünde olan çok sık kullandığınız bir Dictionary varsa, key’in türünü int yapıp, bir sorgu yaparken de
obje.GetInstanceID()‘i key olarak kullanın. Hatta mümkünse bu GetInstanceID’nin sonucunu cache’leyin, örneğin objelerinizi bir List<Object>‘te tutuyorsanız, bunlara karşılık gelen GetInstanceID’leri de bir List<int>‘te tutabilirsiniz. - [5] Key‘i bir enum türünde olan Dictionary‘ler, sorgulama esnasında hafızayı bir miktar harcarlar ve bu da garbage collector‘ı yorar. Bundan kaçınmak için Dictionary’i oluştururken bir IEqualityComparer kullanın.
Optimizasyon öncesi:
private enum TestEnum { A, B };
private readonly Dictionary<TestEnum, int> dict = new Dictionary<TestEnum, int>();
private void Update()
{
int deger;
dict.TryGetValue( TestEnum.A, out deger ); // Her seferinde 20 byte RAM harcar
}
Optimizasyon sonrası:
private class TestEnumComparer : IEqualityComparer<TestEnum>
{
public bool Equals( TestEnum x, TestEnum y ) { return x == y; }
public int GetHashCode( TestEnum obj ) { return (int) obj; }
}
private enum TestEnum { A, B };
// Dictionary'i oluştururken IEqualityComparer kullanıyoruz
private readonly Dictionary<TestEnum, int> dict = new Dictionary<TestEnum, int>( new TestEnumComparer() );
private void Update()
{
int deger;
dict.TryGetValue( TestEnum.A, out deger ); // Artık hiç RAM harcamaz
}
- [8] Hierarchy’de sahnenin root’unda yer alan (parent’ı olmayan) tüm Transform‘ların hierarchyCapacity adında bir değişkeni bulunmaktadır. Unity sahnedeki Transform’ları kendi içinde şu şekilde barındırmaktadır: Her root Transform içerisinde List gibi bir yapı barındırır ve bu List’te de Transform’un tüm child objeleri yer alır (child objelerinin child objeleri, onların da child objeleri vs. bu listeye dahildir). Bu root Transform’a veya onun child’larından birine eklenen yeni bir child obje, root Transform’un içindeki bu List’e dahil olur. Eğer List ağzına kadar doluysa, List’in boyutu artar. Root Transform’ların sahip olduğu hierarchyCapacity, bu List’in kapasitesini belirlemektedir. Gelelim buradaki optimizasyona: eğer sahnedeki bir root Transform’a SetParent ile pek çok child obje ekleyecekseniz ve toplamda kaç child obje ekleyeceğinizi kabaca biliyorsanız, her birkaç SetParent’ta bir Transform’un hierarchyCapacity’sinin otomatik olarak artırılması yerine (kapasite her arttığında bu Garbage Collector’a yük olur), en başta root Transform’un hierarchyCapacity’sini elle belirleyerek otomatik olarak kapasitenin artmasının önüne geçebilirsiniz. Yalnız root Transform’a kaç child ekleyeceğinizi bileceğiniz gibi aynı zamanda her bir child’ın da kaç child’dan oluştuğunu bilmeniz ve hierarchyCapacity’i ona göre artırmanız lazım çünkü başta da dediğim gibi, root Transform’un içindeki List, onun tüm child’larını, child’larının child’larını, onların child’larını vs. kısaca objenin altında yer alan her şeyi içinde barındırır.
- [11] ParticleSystem‘ın Play, Pause gibi fonksiyonları
bool withChildrenisminde opsiyonel bir parametre alırlar; bu parametre varsayılan olarak true‘dur. Parametre true olduğunda, ParticleSystem GetComponentsInChildren ile alt objelerindeki ParticleSystem component’lerini de bulur ve Play/Pause komutlarını onlara da uygular. Tahmin edeceğiniz üzere, GetComponentsInChildren kullanımından dolayı bu performans için iyi değildir. Eğer withChildren parametresinin değeri false ise, GetComponentsInChildren çağrılmaz ve Play/Pause fonksiyonu sadece mevcut ParticleSystem’a uygulanır. Eğer bir ParticleSystem’ın child’ı olmadığını biliyorsanız withChildren’ı daima false yapın, aksi taktirde ParticleSystem’ı ve içindeki tüm child’ları oyunun başında bir array’e alıp, Play/Pause yaparken array’deki tüm ParticleSystem’ların Play/Pause fonksiyonlarını withChildren false parametre ile tek tek çalıştırın. - [9] Bir AudioSource‘un sesini Mute ile kıssanız bile, AudioSource hâlâ müziği sessiz bir şekilde çalarak CPU yemeye devam eder. Mümkünse Stop fonksiyonu ile çalmakta olan sesi durdurun veya
audioSource.enabled=false;ile Audio Source’u disable edin. - [14] Bir fonksiyon params şeklinde bir parametre alıyorsa (mesela
params int[]), bu fonksiyonu her çağırdığınızda arkaplanda yeni bir array oluşturulur ve parametreleriniz bu array’e kopyalanır. Örneğin Mathf.Max ve Mathf.Min fonksiyonlarının 2’den fazla parametre alan versiyonları params kullanır. Bu, özellikle garbage collector için kötü olduğundan, bu fonksiyonları kullanmamaya çalışın. - [15] Garbage Collector‘u
System.GC.Collect();ile elle çalıştırabilirsiniz. Örneğin bir loading ekranı esnasında bu fonksiyonu çağırırsanız, garbage collector’un oyun esnasında çalışıp da oyunu dondurma şansını azaltırsınız. - [4] Çok uzun bir
const stringkullanıyorsanız, bunustatic readonly stringyapın. “const string” build esnasında kodda her kullanıldığı yere kopyala-yapıştır yapıldığı için, kodun şişmesine sebep olabilir. - [9] IL2CPP ile build alırken, compiler otomatik olarak kodunuza şu eklemeleri yapar (kaynak):
- Bir objeye erişmeden önce, o objenin null olup olmadığına bakan bir if koşulu
- Array’in herhangi bir elemanına erişmeden önce, o index’in array’in
[0,array.Length)sınırları dahilinde olup olmadığına bakan bir if koşulu
IL2CPP kodunuzu C++’a çevirdiği için, bu if koşullarını otomatik olarak eklemeseydi, bu koşullardan birinde bir sıkıntı çıkınca (eriştiğiniz obje null ise veya array’in -1. index’teki elemanına erişmeye çalışırsanız) oyununuz çökerdi. Ancak bazen bu ekstra if koşullarının performans harcamaktan başka işe yaramadığı, yazdığınız kodun bu iki sıkıntıyı kesinlikle yaşamayacağını bildiğiniz durumlar olacaktır. Bu durumlarda isterseniz IL2CPP’nin kodunuzun o güvenli kısımlarına otomatik olarak if koşulları eklemesini engelleyebilirsiniz. Bunun için Unity’nizin kurulu olduğu konumdaki Editor\Data\il2cpp\Il2CppSetOptionAttribute.cs dosyasını kopyalayıp projenizin Assets klasörüne yapıştırın. Ardından şu iki attribute ile, istediğiniz class, fonksiyon veya property‘de bu if koşullarının oluşmasını engelleyebilirsiniz (kodun başına using Unity.IL2CPP.CompilerServices; da ekleyin):
private float[] array = new float[5];
[Il2CppSetOption( Option.NullChecks, false )] // Null kontrolünü kapat
[Il2CppSetOption( Option.ArrayBoundsChecks, false )] // Array index'inin sınırlar dahilinde olup olmadığı kontrolünü kapat
private float ArrayElemaniDondur( int index )
{
return array[index];
}
Fizik Optimizasyonu
- [10][16] Bazı oyunlarda fizik çok önemli bir yere sahipken bazı oyunlarda ise fiziksel etkileşimler minimum düzeydedir (hatta belki de hiç fizik kullanılmamaktadır). Fizik olaylarının hesaplaması FixedUpdate‘te yapılır ve FixedUpdate fonksiyonu da belli bir frekansta çalıştırılır. Eğer Edit-Project Settings-Time‘a girecek olursanız, fizik hesaplamalarının varsayılan olarak 0.02 saniyelik bir periyotla gerçekleştiğini görebilirsiniz (Fixed Timestep). Eğer ki oyununuzda fizik olayları çok büyük bir yere sahip değilse, bu değeri yavaş yavaş artırıp bunun oyununuzdaki fiziksel etkileşimlere olan etkisini kontrol edin. Bu değeri ne kadar artırırsanız, bu performans için o kadar iyidir ama eğer değeri çok artırırsanız da o zaman fiziksel etkileşimler istendiği gibi çalışmamaya başlar, o yüzden deneye yanıla ince ayar yapmanız en iyisi.
- [10] Bir objenizde collider var ve bu obje oyun sırasında hareket mi ediyor? O zaman ya bu objede ya da bu objenin bir parent’ında bir Rigidbody de olduğundan emin olun (objenin fiziksel güçlerden [yerçekimidir, AddForce’dur vb.] etkilenmesini istemiyorsanız da Rigidbody’deki “Is Kinematic“i işaretleyin). Aksi taktirde Rigidbody’siz ama collider’lı bu obje her hareket ettiğinde, fizik motoru bazı ağır hesaplamalar yapmak zorunda kalır.
- [10][16] Fizik objelerinizde mümkün olduğunca az Mesh Collider kullanın. Mesh Collider kullanan çoğu objenin şeklini aslında birkaç primitive collider (Box Collider, Sphere Collider ve Capsule Collider) kullanarak da yaklaşık olarak taklit edebilirsiniz (ki bu “yaklaşık” olarak taklit etme durumu, çoğu fiziksel etkileşim için yeterli olur). O halde napıyoruz? Objelerimizdeki Mesh Collider’ları, Unity’nin sağladığı primitive collider’lar ile taklit etmeye çalışıyoruz (ardından Mesh Collider component’ini silmeyi unutmayın). İhtiyaç halinde bu collider’ları, objenin child GameObject’lerine de verebilirsiniz (mesela collider’a biraz eğim vermeniz gerekiyorsa).
- Raycast fonksiyonlarınızda, bu raycast ışını için bir maksimum uzunluk belirleyin. Çoğu durumda sonsuz uzunlukta bir raycast ışını yollamak yerine örneğin 100 birim uzunluğunda bir raycast ışını yollamak yeterlidir. Bir başka önemli husus ise layer mask kullanımı. Sahnedeki tüm objeler üzerinden raycast testi yapmak yerine sadece belli bir layer’daki objelere karşı raycast yaparsanız, tahmin edebileceğiniz üzere daha hızlı sonuç alırsınız. Örneğin bir karakterin havada mı yoksa yerde mi olduğunu kontrol etmek istiyorsanız gökyüzündeki bulutları raycast’e karıştırmanıza gerek yok, sadece zemin objelerine karşı raycast yapmanız yeterli. Bunun için zemin objelerine “Zemin” isminde bir layer verip bu layer’ı Raycast fonksiyonunuzda kullanabilirsiniz.
Optimizasyon öncesi:
// Karakter yerde mi kontrol ediyoruz
public bool IsGrounded()
{
// Karakterin pozisyonundan aşağı yönde 1 birim uzunluğunda
// raycast ışını yollayıp herhangi bir objeye çarpıp çarpmadığına bak
return Physics.Raycast( transform.position, Vector3.down, 1f );
}
Optimizasyon sonrası:
// Bu değişkene Inspector'dan değer olarak "Zemin" layer'ı verilmeli
public LayerMask zeminLayer;
// Karakter yerde mi kontrol ediyoruz
public bool IsGrounded()
{
// Karakterin pozisyonundan aşağı yönde 1 birim uzunluğunda
// raycast ışını yollayıp herhangi bir zemin objesine çarpıp çarpmadığına bak
return Physics.Raycast( transform.position, Vector3.down, 1f, zeminLayer );
}
- Physics.RaycastAll ve Physics.OverlapSphere gibi fonksiyonlar her seferinde yeni bir array döndürerek cihazın hafızasını yavaş yavaş sömürürler (bu sömürme durumu Physics.Raycast için geçerli değil). Bu fonksiyonların çoğunun, hafızadan yemeyen NonAlloc isimli versiyonları bulunmaktadır: Physics.RaycastNonAlloc ve Physics.OverlapSphereNonAlloc. Bu NonAlloc fonksiyonlar parametre olarak bir RaycastHit[] array’i alırlar ve buldukları RaycastHit sonuçları bu array’de depolarlar. Fonksiyonun döndürdüğü int değeri ise, kaç RaycastHit bulunduğunu (haliyle array’e kaç RaycastHit eklendiğini) belirtir. Diyelim fonksiyona kapasitesi 10 olan bir RaycastHit[] array’i verirseniz ve fonksiyon 3 döndürürse, fonksiyonun döndürdüğü RaycastHit’ler array’in 0, 1 ve 2. index’lerinde depolanır. Eğer fonksiyona çok küçük bir array verirseniz (bu örnekte diyelim 2 elemanlı bir array), fonksiyon 2 RaycastHit bulduktan sonra otomatik olarak durur, diğer RaycastHit’leri bulmaz.
- [16] Edit-Project Settings-Physics‘teki Layer Collision Matrix, hangi layer’larda yer alan objelerin birbiriyle temas edebileceğini belirler. Birbiriyle kesinlikle temas etmeyecek layer’ların tiklerini kaldırarak, boş yere bu layer’lar arası temas hesaplamaları yapılmasını engelleyebilirsiniz. Eğer 2D fizik kullanıyorsanız, aynı matrix Edit-Project Settings-Physics 2D‘de de mevcuttur.
- [16] Edit-Project Settings-Physics‘teki Auto Sync Transforms‘u kapatın. Bu değer açık olduğunda, bir objenin Transform’u ne zaman değişirse fizik motoru bu değişikliği anında fizik dünyasına uygularken, bu değeri kapatırsanız tüm Transform değişiklikleri FixedUpdate’ten hemen önce toplu bir şekilde fizik dünyasına uygulanır. Oyununuzda çok fazla Rigidbody varsa ve Update’te sürekli Transform değerlerini değiştiriyorsanız, bu optimizasyonun performansa büyük etkisi olabilir.
- [16] Edit-Project Settings-Physics‘teki Reuse Collision Callbacks‘i açın. Kodlarınızdaki OnCollisionEnter gibi fonksiyonların aldıkları
Collision temasparametresi, her seferinde yeni bir Collision objesi oluşturarak yavaş yavaş hafızadan yer. Ancak eğer bu seçeneği açarsanız, fizik motoru tek bir Collision objesini tekrar tekrar kullanır, sürekli yeni Collision objesi oluşturmaz. Bu durumun tek dezavantajı, eğer Collision parametresini script’inizde daha sonra kullanmak üzere bir yerde depoluyorsanız, bu Collision objesinin içeriği bir sonraki temasta değişeceği için kodunuz düzgün çalışmayabilir. Ancak ben şimdiye kadar hiç Collision objesini depolama ihtiyacı hissetmediğimden, bu handikapın çoğu proje için önemsiz olduğunu düşünüyorum.
UI Optimizasyonu
- [2] UI’ın daha performanslı çalışması ve sprite’larınızın daha az yer kaplaması için, UI’de sprite atlas kullanın. Neyse ki Unity bu konuda bize oldukça yardımcı oluyor. Edit-Project Settings-Editor‘de yer alan Sprite Packer‘ı Always Enabled‘a veya Enabled For Builds‘a çekin. İlki, Unity’nin editörde de sprite atlasları kullanmasını sağlarken ikincisi, sprite atlas’ların sadece oyunu build alırken oluşmasını sağlar; ben ilkini kullanıyorum. Bundan sonraki aşama, Unity sürümünüze göre değişiklik gösteriyor:
- Unity 2017.1 ve sonrası: Project panelinde Create-Sprite Atlas ile yeni bir sprite atlas oluşturun. Allow Rotation ve Tight Packing değerlerini kapatın (UI’da sıkıntı çıkarabiliyor) ve sprite atlas’a eklemek istediğiniz sprite’ları, Objects for Packing kısmına ekleyin. Buraya bir klasör eklerseniz, klasörün içindeki tüm sprite’lar otomatik olarak sprite atlas’a dahil olur. Dilerseniz Pack Preview butonuna tıklayarak, oluşan sprite atlas’ı görüntüleyebilirsiniz.
- Daha eski sürümler: Sprite Atlas’a eklemek istediğiniz sprite’ların Inspector’larındaki Packing Tag‘lerine istediğiniz gibi bir değer verin (mesela “UIAtlas“). Aynı Packing Tag’i taşıyan sprite’lar, otomatik olarak aynı sprite atlas’a eklenirler.
- [5][13][16] Tüm UI objelerinizi tek bir Canvas‘ın içine yığmayın. Canvas’ın içindeki tek bir UI elemanının tek bir özelliği değişse (konumu, rengi vs.), tüm canvas yenilenmek zorunda kalır (evet, TÜM CANVAS). Örneğin dinamik UI objeleriniz ile statik (hiç değişmeyen) UI objelerinizi ayrı canvas’larda tutun. Dinamik UI objelerinizden bazıları çok sık değişiyor ama bazıları ender değişiyorsa, bu çok sık değişen UI objelerinizi de ayrı bir canvas’a alın. Burada işin güzel yanı, bu iki canvas’ın birbirinden tamamen bağımsız olmasına gerek yok, bir canvas objesi başka bir canvas objesinin child objesi olabilir; buna nested canvas denir. Parent canvas’taki bir UI objesi değişse bile, nested canvas’taki UI objelerinde bir değişiklik olmadığı sürece nested canvas yenilenmez, boş yere CPU harcanmaz. Nested canvas’larla ilgili dikkat etmeniz gereken tek şey, eğer nested canvas’ın içinde Button gibi, mouse veya parmak ile etkileşime girebilen bir UI objesi varsa, nested canvas’ta Graphic Raycaster component’i olduğundan emin olun; parent canvas’taki Graphic Raycaster nested canvas’ı etkilemez.
- [2] Canvas’ınızın Hierarchy’sinde objelerinizin sırası Image-Text-Image-Text diye gidiyorsa, bunu mümkün olduğunca Image-Image-Text-Text şeklinde yeniden düzenlemeye çalışın. Yani Text’leri Hierarchy’de elinizden geldiğince alt alta tutmaya çalışın, araya Image vari başka bir component sokmayın. Bu şekilde Unity’nin UI’ı daha iyi optimize ederek draw call sayısını azaltma şansı daha fazla olacak.
- [5][16] Horizontal Layout Group ve Grid Layout Group gibi Layout Group component’lerini mümkün olduğunca az kullanın, canvas her kendini yenilediğinde bu component’ler canvas’a ekstra yük olurlar. Arayüzünüzün düzenli olması için bu Layout Group’lar kritik öneme sahipse ama içerikleri dinamik değilse (bir kere oluştuktan sonra değişmeyecekse), arayüzünüzü ekranda gösterdikten sonra bu component’leri disable ederek (
.enabled=false), canvas’ın bundan sonraki kendini yenileme sürecinde bu component’lerin boş yere aynı hesaplamaları yapmalarının önüne geçebilirsiniz. - Yeni bir Scroll View oluşturduğunuzda, oluşan objenin Viewport isimli child’ında Mask da dahil olmak üzere bir takım component’ler yer alır. Mask’ın amacı, scroll view’ın içeriğinin, Viewport’un dışında kalan kısmını ekrana çizdirmemektir. Artık aynı işi daha performanslı bir component ile yapmak mümkündür: Rect Mask 2D. Bu değişiklik için Viewport’taki RectTransform hariç tüm component’leri silip ardından Add Component ile objeye Rect Mask 2D component’i eklemeniz yeterli.
- [2] Text component’inin Best Fit seçeneğini çok lüzumlu olmadıkça kullanmayın çünkü yazının font boyutunun hesaplamak için CPU’nun bir takım deneme yanılma yapması gerekmektedir.
- (Unity 2018.3+) [1][16] İçerisinde çok fazla transparan piksel olan bir sprite arayüzde büyük bir yer kaplıyorsa, bu overdraw sıkıntısına zemin hazırlarlar. Overdraw, objelerin üst üste çizilmesinin GPU’yu zorlaması sıkıntısıdır. Ne kadar çok obje üst üste çizilirse, grafik performansı o kadar düşer. Image‘larınızda kullandığınız sprite‘lardaki transparan pikseller her ne kadar görünmez olsalar da, GPU tarafından işlendikleri için overdraw’dan kaçamazlar. Neyse ki Unity’nin son sürümleriyle beraber, Image component’ine Use Sprite Mesh değişkeni eklendi (yalnızca Image Type‘ın değeri Simple iken gözükür). Bu ayarı aktif ettiğinizde, artık Image ekrana bir quad (dikdörtgen) geometri ile çizilmek yerine, sprite import edilirken otomatik olarak oluşturulan optimize geometri ile çizilir (sprite’ın Inspector’undaki Mesh Type‘ın Tight olması lazım, oluşan geometri transparan piksellerin büyük kısmını dışarıda bırakır). Bu, ekrana biraz daha fazla vertex ve triangle çizilmesi anlamına gelir ama overdraw genelde vertex sayısından daha büyük bir sıkıntı olduğu için, bu bir sorun olmaz.
- [16] Yukarıda bahsettiğim overdraw (objelerin üst üste çizilmesi) sıkıntısından dolayı, bir Image‘ın üzerine başka bir Image veya Text‘i olabildiğince az çizdirmeye çalışın. Örneğin Hearthstone vari bir kart oyununda, kartın her bir bileşenini ayrı bir Image veya Text yapmak, kartı kişiselleştirmeyi kolaylaştırır ama bir yandan da overdraw’u artırır. Böyle bir durumda, sizi de çok zora sokmayacak şekilde, üst üste çizilen sprite ve text’leri olabildiğince birleştirmeye, daha az Image ve Text kullanmaya çalışın. Örneğin her kartın PNG dosyası, o kartın üzerindeki yazıları vs. içine dahil ederse, tek bir Image ile kart ekrana çizilebilir.
- [5] Canvas‘ın Pixel Perfect‘i ekstra performans harcar, lüzumlu değilse bu seçeneği kapalı tutun.
- [5] Canvas‘larınızda Render Mode olarak Screen Space – Camera yerine mümkünse Screen Space – Overlay‘i tercih edin.
- [5][13] Render Mode‘u World Space olan Canvas‘larınızın Event Camera‘sını asla boş bırakmayın, buraya kameranızı değer olarak verin. Aksi taktirde Unity arkaplanda defalarca kez Camera.main‘i çağırmak zorunda kalır.
- [13][16] Mouse veya parmak ile herhangi bir şekilde etkileşime geçmeyen Image, Text vs. objelerin Raycast Target‘larını kapatın. Örneğin yeni bir Button oluşturunca, içinde otomatik olarak bir Text de oluşur; bu Text’te Raycast Target’a gerek yoktur.
- [5][13] Bir Canvas‘ı gizlemek için SetActive(false) yerine enabled=false‘u tercih edin, canvas’ı bu şekilde gösterip gizlemek daha performanslıdır. Ancak enabled=false yaptığınızda, canvas’ın child objelerinde Update fonksiyonu olan script’ler varsa bunlar çalışmaya devam edecektir, bu yüzden Canvas’ın enabled’ını değiştirdikten hemen sonra bu script’lerin enabled’ını da değiştirmek isteyebilirsiniz.
- [5][16] Scroll View‘ınızda çok fazla (yüzlerce veya binlerce) eleman varsa, sadece ekranda gözüken elemanlar için UI objesi oluşturan bir sistem kullanın (list view diye de geçer); aksi taktirde o yüzlerce eleman, canvas’ın sınırları dışında kalsalar bile bir miktar performans yerler. Bu konuda yazdığım ders: https://yasirkula.com/2020/10/14/unity-scroll-viewda-cok-sayida-objeyi-performansli-bir-sekilde-gostermek/
- [5] Scroll Rect component’inin Inertia değeri açık olduğunda, scroll view’ı biraz kaydırıp parmağınızı çektiğinizde, scroll view hâlâ kaymaya devam eder ve bir süre sonra yavaşlayarak durur. Eğer Inertia kullanıyorsanız, Deceleration Rate‘i olabildiğince düşürün (mesela 0.001). Bu değer ne kadar düşerse, scroll view o kadar hızlı yavaşlayıp durur. Scroll view kaydığı sürece canvas sürekli kendini yenilemek zorunda olduğu için, bu süreyi kısaltmak canvas’ı rahatlatır.
- [5] Eğer UI elemanlarınızdan bazılarında object pooling‘den faydalanıyorsanız ve bir UI objesini havuza atarken aynı zamanda SetParent yapıyorsanız (illa gerekli değilse yapmayın), SetParent’tan hemen önce objeyi inaktif yapın. Havuzdan objeyi çekerken ise önce SetParent yapın, ardından objede herhangi bir değişiklik yapacaksanız bu değişiklikleri yapın ve ancak ondan sonra objeyi aktif hale getirin.
Grafik Optimizasyonu
- Oyununuzdaki texture’ların boyutu düşük olsun diye onları Unity’e .jpeg formatında atmayın. Ya da benzer şekilde, aslı 512×512 olan kaliteli bir texture’u, boyutu düşük olsun diye resim editörünüzden (Photoshop vb.) 256×256’ya ufaltıp öyle Unity’e yollamayın. Unity’e elinizdeki en kaliteli kaynak dosyayı yollayın (örneğin .psd veya .png ve 512×512). Siz bir resim dosyasını Unity’e hangi formatta atarsanız atın bu resim dosyası Unity içerisinde otomatik olarak bir texture‘a dönüştürülür. Bu esnada bu resim dosyanın kaç MB olduğunun texture’un boyutuna bir etkisi olmazken, resim dosyasının kalitesi, Unity tarafından oluşturulan texture’un kalitesine direkt olarak etki eder. Siz Unity’e 512×512’lik bir resim dosyası atsanız bile, bunu Inspector‘da en altta yer alan “Max Size“dan otomatik olarak 256×256’lık bir texture’a da dönüştürebilirsiniz.
- Mobil platformlarda Player Settings‘teki Resolution Scaling Mode‘u Fixed DPI yapın ve Target DPI değerini de 320 yapın. Bu ayar, oyunun maksimum 320dpi çözünürlüğünde ekrana çizilmesini sağlar. Özellikle retina ekranlar ufak olmalarına rağmen bilgisayar çözünürlüklerinin bile üzerinde çözünürlüklere sahip olabildikleri için, bu ekranlarda render almak çok zaman alabilir. Ancak oyunu maksimum 320dpi’da render alarak bu süreci epey hızlandırabilirsiniz. Dilerseniz 320dpi değerini, oyunun görselliğinde büyük bir fark olmadığı sürece daha da düşürmeyi deneyebilirsiniz.
- Minimum sayıda materyal kullanın. Oyun esnasında kameranın görüş alanındaki farklı materyal sayısı ne kadar çok olursa, “SetPass calls” dediğimiz değer de o kadar çok olur; bu da oyunun yavaşlamasına sebep olur.
- [16] Shader’larınızı olabildiğince basit tutun/seçin. Yeni oluşturulan materyallere otomatik olarak atanan Standard Shader‘ı minimum düzeyde kullanın (mobil oyunlarda hiç kullanmamaya çalışın). Bu shader, PBR adı verilen gerçekçi ışıklandırmaya yönelik hesaplamalar üzerine kurulduğu için, özellikle mobil platformlarda çok fazla performans harcar. Materyaliniz için bir shader seçerken öncelikle Mobile altında listelenmiş shader’lara bakın, burada ihtiyacınızı karşılayan bir shader yoksa o zaman Legacy Shaders‘a yönelin.
- Dinamik (gerçek zamanlı) gölgelendirmeden mümkün olduğunca az faydalanın. Eğer sahnenizdeki sabit duran (static) objeler bile dinamik gölge düşürüyorsa burada performans açısından büyük bir sıkıntı söz konusu. Sahnenizdeki sabit objeleri (ev, duvar, masa vb. yeri kesinlikle hiç değişmeyecek ve oyun sırasında kesinlikle yok olmayacak objeler) Inspector‘dan “Static” yapın ve onlara lightmapping uygulayın. Hem çok daha kaliteli gölgeler elde edersiniz hem de oyunu çalıştıran cihaz derin bir “oh” çeker.
- Game panelinin sağ üstündeki Stats kapalıysa açın ve oyun boyunca bir gözünüzü “Batches” ve “SetPass calls“ta tutun. Bu değerler ne kadar az olursa o kadar iyi. “SetPass calls” değeri, kameranın görüş alanındaki objelerde ne kadar çok farklı materyal varsa o kadar artar. “Batches” değeri ise, kameranın görüş alanındaki objelerin sayısı ile doğru orantılı olarak artar. Gölgelerin açık olduğu durumda Batches değeri yaklaşık ikiye katlanabilir. Bu iki değerin optimizasyonunda önceliği “SetPass calls”a vermelisiniz çünkü bu değerin yüksek olması, “Batches”ın yüksek olmasına göre performansa daha çok etki eder. “SetPass calls” değerini düşürmenin tek yolu var: olabildiğince az materyal kullanmak; yani olabildiğince çok objeye aynı materyali vermelisiniz.
- (Özellikle mobil) [15] Eğer sahnenizde aynı materyali taşıyan ve low-poly olan çok fazla obje varsa, Player Settings‘teki Dynamic Batching‘i açarak “Batches“ın azalmasına yardımcı olabilirsiniz. Bu ayar, Unity’nin kameranın görüş alanındaki aynı materyale sahip objeleri GPU’ya yollamadan önce birleştirerek tek bir obje yapmasını sağlar, böylece birden çok obje tek bir seferde çizilir. Bir frame’de kaç objenin birleştirildiğini, Stats ekranında “Saved by batching” olarak görebilirsiniz.
- Eğer oyununuz kapalı bir alanda veya bir şehirde geçiyorsa, Occlusion Culling kullanarak bir duvarın veya binanın arkasında kalan objelerin boş yere GPU’ya yollanmasını engelleyin. Occlusion Culling dersim için: https://yasirkula.com/2020/03/31/unity-occlusion-culling-sistemi/
- Farklı texture’lara sahip objelerin aynı materyali kullanmaları için “texture atlasing” tekniğinden faydalanın. Yalnız bunun için 3D modelleme konusunda biraz bilginizin olması ya da Asset Store’daki plugin’lerden birine sahip olmanız lazım.
- Çoğu 3D modelleme uygulaması, modeli “vertex color” ile boyamanıza imkan verir. Eğer ki modeliniz sadece düz renklerden oluşuyorsa, ona texture vermek yerine sadece vertex color uygulayabilirsiniz. Bu durumda objenin Unity’deki shader’ını, vertex color destekleyen bir shader ile değiştirmeyi unutmayın (bunun için “unity vertex color diffuse shader” gibi arama yapabilirsiniz).
- [1] 3D modellerinizde, oyuncunun hiç göremeyeceği yüzleri (triangle) silin. Örneğin bardak modelinizin alt yüzü oyun esnasında hiç gözükmüyorsa, oradaki triangle’ları silin gitsin.
- [1][16] Özellikle mobilde Image Effect (post processing) kullanımını olabildiğince minimum düzeyde tutun. Bu efektler sandığınızdan daha fazla performans harcayabilir.
- Fog (sis) kullanmak da performans için iyi değilmiş. Eğer fog, oyununuzda çok ciddi bir yere sahip değilse kullanmaktan kaçının.
- Bump mapping (normal mapping), bir objeye düşen ışığın çok daha gerçekçi durmasına yardımcı olabiliyor ancak tahmin edebileceğiniz üzere bunu yaparken de ekstra performans yiyor. Eğer kullanacaksanız sadece çok kritik objeler için normal mapping kullanın (mesela player’ın elinde tuttuğu silah objesi için). Bu objelerin normal map texture’larının Max Size‘ını, objenin görünümüne büyük bir etki etmeyecek şekilde olabildiğince düşürmeye çalışın.
- [1] Unity’nin son sürümleri ile gelen dinamik skybox yerine, cubemap kullanılarak hazırlanmış eski usül skybox’lar kullanın. Yeni skybox dinamik olarak oluşturulduğu için performans açısından daha kötüdür.
- Eğer ki oyununuzda skybox (gökyüzü) kullanmıyorsanız şu ayarları yapın:
Main Camera-“Clear Flags“: “Solid Color”
Window-Lighting-Skybox: “None”
Window-Lighting-Ambient Source: “Color”
Window-Lighting-Reflection Source: “Custom” (reflective shader kullanmıyorsanız Cubemap‘i “None” olarak bırakın)
- Window-Lighting‘de “Precomputed Realtime GI” ve “Baked GI” adında 2 seçenek bulunmaktadır. Bu seçenekler lightmapping ile alakalıdırlar. Aralarındaki fark ise şöyle ki: “Precomputed Realtime GI”da ortamdaki ışık sayısı, ışıkların rengi veya şiddeti dinamik olarak değişse bile sahnedeki ışıklandırma ona göre otomatik olarak güncellenirken, “Baked GI”da ortamdaki ışıkların durumu değişse de lightmap sabit kalır.
Çoğu oyunda sahnedeki ışıklar sabit olduğu için “Baked GI” doğru tercihtir. İlaveten, “Baked GI” ile oluşturulan lightmap görsel olarak daha kaliteli olur. Yani dikkat etmeniz gereken noktalar şunlar:
– oyunda lightmapping kullanmayacak mısınız (çoğu basit oyunda kullanmazsınız)? O halde hem “Precomputed Realtime GI”ı hem de “Baked GI”ı kapatın
– lightmapping kullanacaksınız ve sahnedeki ışıklandırma dinamik olarak değişecek mi? O zaman “Precomputed Realtime GI”ı açık bırakırken “Baked GI”ı kapatın
– lightmapping kullanacaksınız ve sahnedeki ışıklandırma sabit mi kalacak? O zaman “Baked GI”ı açık bırakırken “Precomputed Realtime GI”ı kapatın
– lightmapping kullanacaksınız ve oyunu mobil platformlara mı tasarlıyorsunuz? O zaman çok zorunda kalmadığınız sürece “Precomputed Realtime GI” kullanmayın
- [1] Lightmapping kullanıyorsanız, çok ufak statik objeleri bu sürece dahil etmeyin çünkü obje çok ufak olduğu için lightmapping’in etkisi belki de belli bile olmayacak, ama obje boş yere lightmap texture’larınızın boyutunu artıracak. Onun yerine bu objeleri light probe‘lar ile ışıklandırın (illa ışıklandıracaksanız). Bir statik objeyi lightmapping’den çıkarmak için, objenin Inspector’undaki Static işaretinin yanında yer alan oka tıklayın ve Lightmap Static‘i kapatın (yeni Unity sürümlerinde Contribute GI).
- (Android) Player Settings-Resolution and Presentation‘daki “Use 32-bit Display Buffer” seçeneğini kapatarak oyununuzu mobil cihazınızda test edin. Eğer grafiklerde göze batan bir değişiklik olmadıysa bu seçeneği kapalı bırakın.
- (Özellikle Android) Eğer ki özellikle OpenGLES3 gerektiren bir shader kullanmadığınızı düşünüyorsanız Player Settings-Other Settings‘teki “Auto Graphics API” seçeneğini kapatın ve “Graphics APIs” listesinden OpenGLES3’ü kaldırıp listede sadece OpenGLES2‘yi bırakın ve oyunu test edin. Eğer göze batan bir değişiklik olmadıysa bu ayarı böyle bırakın. Forumlarda, bu değişikliğin performansa çok etki ettiğini söyleyenler var.
- (Özellikle mobil) Player Settings‘teki Color Space‘i Gamma‘da tutmaya çalışın. Alternatif seçenek olan Linear daha gerçekçi ışıklandırma sağlar ama daha yavaştır.
- [1] Player Settings‘te Multithreaded Rendering açık değilse açın.
- Bazı insanlar Unity’de Vsync‘i açınca oyunlarının daha hızlı hale geldiğini iddia ediyorlar. Bunu kesin olarak bilmiyorum ancak denemek isterseniz Edit-Project Settings-Quality yolunu izleyip, build aldığınız platformun varsayılan grafik düzeyine geçiş yapın (varsayılan düzey yeşil bir tik ile gösterilir). Ardından “V Sync Count“u “Every V Blank” yapın ve oyunu test edin.
- Gerçek zamanlı (dinamik) gölgeler çok fazla performans harcar ve bunları olabildiğince optimize etmek veya sayılarını azaltmak büyük önem arz eder. Unity’de “Hard Shadows” ve “Soft Shadows” olmak üzere iki farklı gölgelendirme yöntemi mevcut. “Hard Shadows” daha kaba durur ancak daha az performans harcar. Bu yüzden, illa dinamik gölge kullanacaksanız, görsel olarak çok çok büyük bir fark olmadığı sürece ışıklarınızda “Hard Shadows” kullanın.
- Eğer gölge kullanıyorsanız, Edit-Project Settings-Quality‘den Shadow Distance‘ı olabildiğince düşürün. Bu değer, gölgelerin çizileceği en uzak mesafeyi belirler. Daha uzaktaki objelerin gölgeleri çizilmez. Quality ayarlarındaki Shadow Cascades‘i de, görsele çok büyük etkisi olmadığı sürece No Cascades yapın.
- [4][14] Eğer sahnenizde Light Probe veya Reflection Probe kullanmıyorsanız, objelerinizin Mesh Renderer‘larının Light Probes ve Reflection Probes değerlerini Off yapın. İlaveten “Motion Vectors” değerini de “Force No Motion” yapın. Mümkün olduğunca Cast Shadows (objenin gölge bırakmasını sağlar) ve Receive Shadows (diğer objelerin gölgelerinin bu objenin üzerine düşmesini sağlar) değerlerini kapatın. Örneğin çok ufak objelerin veya gölgesi belli bile olmayan objelerin gölge hesaplamaları yüzünden boş yere performansınızı düşürmelerine izin vermeyin.
- [8][14] 3D model asset’lerinizin Inspector’daki Optimize Mesh değerinin açık, Read/Write Enabled değerinin kapalı olduğundan emin olun. Eğer objeye Mesh Collider verecekseniz o zaman “Read/Write Enabled”ı açık bırakmak isteyebilirsiniz.
- [15] Bir 3D modeli hiçbir zaman normal map ile kullanmıyorsanız, Inspector’daki Tangents‘ı None yapın. Eğer modeli Unlit materyal harici bir materyalle kullanmıyorsanız, o zaman Normals‘ı da None yapın. Böylece model, kullanmadığınız bu verilerden arınmış olacak.
- [8] Animasyona sahip 3D modellerinizin Inspector’daki Rig sekmesinde yer alan Animation Type değeri Generic veya Humanoid ise, aynı sekmedeki Optimize Game Objects‘i etkinleştirin. Bu şekilde 3D modelin child objeleri Unity tarafından optimize edilip Hierarchy’den gizlenir. Eğer belli başlı child objelerin Hierarchy’den gizlenmesini istemiyorsanız, onları Extra Transforms To Expose listesine ekleyebilirsiniz. Eğer ki “Optimize Game Objects” seçeneğini değiştirdiğiniz bir 3D model halihazırda bir prefab’ın parçası ise, 3D modeli o prefab’dan silip tekrar eklemeniz gerekebilir çünkü child objelerin Hierarchy’den gizlenmesi değişikliği prefab’lara otomatik olarak uygulanmaz.
- [9][14] Animasyona sahip olmayan 3D modellerinizin Rig sekmesindeki Animation Type‘ını None yapın. Animasyona sahip modellerde ise, inverse kinematics (IK) veya animation retargeting kullanmıyorsanız, Humanoid yerine Legacy (tercihen) veya Generic Animation Type kullanın.
- [16] LOD (Level of Detail) sistemini kullanarak, uzaktaki objelerin daha basit geometriler ile çizilmesini sağlayın. Bu optimizasyon, özellikle büyük çaplı PC oyunlarında çok önemlidir. LOD dersim için: https://yasirkula.com/2020/10/17/unity-lod-sistemi/
- 2D bir oyun yapıyorsanız, UI Optimizasyonu kısmında bahsettiğim Sprite Atlas tekniği 2D Sprite’larınızda da işe yarar. Bu sefer oluşturacağınız Sprite Atlas asset’lerinizin Allow Rotation ve Tight Packing değerlerini açık bırakabilirsiniz, bunlar UI dışında bir sıkıntı çıkarmazlar.
- [15] Belki çok basit kaçacak ama, Edit-Project Settings-Quality‘den oyunun grafik ayarını düşürmek (build alınan platformun Default grafik ayarını değiştirmek) belki de oyununuz için fazlasıyla yeterli olabilir. Default grafik ayarı, yeşil bir tik ile gösterilir.
- Oyununuzun görseline büyük etkisi olmadığı sürece kameranızın Allow HDR ve Allow MSAA değerlerini kapatın. HDR genelde bloom vari post-processing efektlerde kullanılırken, MSAA ise anti-aliasing’i aktifleştirir. Eğer illa anti-aliasing kullanacaksanız, Edit-Project Settings-Quality‘de Anti Aliasing‘in açık olduğundan emin olun.
- (Android) Eğer anti-aliasing kullanmıyorsanız, Player Settings‘teki Blit Type‘ı Auto yapmayı deneyin (sadece 2018.3 ve üstü için).
- Texture‘larda size-of-2 diye bir kural var. Bilgisayarlar en temelinde binary (0 ve 1) üzerine kurulu olduğundan dolayı her türlü işlem aslında 2 tabanında gerçekleşir. Belki de bu yüzdendir ki oyun motorları, texture ebatları olarak hep 2’nin katlarını tercih eder (16, 32, 64, 128, 256…). Eğer ki Unity’e 2’nin katlarından oluşmayan ebatlarda bir texture verirseniz, Unity bu texture’u kendisi 2’nin bir katına çevirir. Yani isteseniz de istemeseniz de aslında size-of-2 texture’lar kullanıyorsunuz (bu durum sprite’lar için geçerli değil). Bu yüzden texture oluştururken kendiniz size-of-2 kuralına uymaya çalışın. Bu şekilde texture’u Unity’de efektif bir şekilde compress edebilme (sıkıştırma) şansına da sahip olacaksınız. Yok illa ki 75×75 texture kullanacağım diyorsanız da 128-75=53 piksellik boş alanı başka bir texture için kullanmayı düşünebilirsiniz (texture atlasing).
- Kameranızın Far Clipping Plane değerini olabildiğince azaltın. Bu değer, kameranızın ekrana çizdireceği objelerin kameradan maksimum ne kadar uzakta olabileceğini belirler. Daha uzaktaki objeler ekrana çizilmez. Bu optimizasyon aynı zamanda z-fighting denilen sıkıntıyı gidermeye de yardımcı olur.
- [1] Camera‘nın çok bilinmeyen bir değişkeni bulunmaktadır: layerCullDistances. Bu değişken vasıtasıyla, kameranın Far Clipping Plane‘inin her layer için farklı olmasını sağlayabilirsiniz. Bu şekilde, sahnenizdeki ufak objeleri farklı bir layer’a alıp bu layer’ın ekrana çizilme mesafesini ufak bir değer yaparsanız, diğer objelerin aksine bu objeler görüş alanından daha çabuk kaybolurlar (ekrana çizilmeyen her obje artı performans demektir), ama zaten ufak oldukları için oyuncu bunu anlamaz bile. Daha fazla bilgi için: https://docs.unity3d.com/ScriptReference/Camera-layerCullDistances.html
- Eğer oyununuzdaki sahnelerde genel olarak hep aynı shader’ları kullanıyorsanız, Player Settings‘teki Keep Loaded Shaders Alive seçeneğini işaretleyerek, sahneler arası geçişlerde shader’ların hafızadan silinmesini engelleyebilirsiniz. Bu şekilde, her yeni sahne açılışında aynı shader’lar tekrar tekrar hafızadan silinip hemen ardından tekrar hafızaya alınmakla uğraşılmaz, shader’lar hep hafızada kalırlar.
Oyun Boyutu (Build) Optimizasyonu
NOT: Unity 2018.2‘den itibaren, şu plugin’i kullanarak oyununuzda en çok hangi asset’lerin yer kapladığını görebilirsiniz: https://assetstore.unity.com/packages/tools/utilities/build-report-inspector-for-unity-119923
- Oyunlarda en çok boyutu genellikle texture‘lar ve müzik dosyaları kaplar. Daha ufak bir build için bu dosyaların Inspector’daki compression ayarlarında olabildiğince ince ayar yapın.
- Resources veya StreamingAssets klasörlerine koyduğunuz asset’ler, hiç kullanılmasalar bile oyuna dahil olurlar ve oyunun boyutuna olumsuz etki ederler. Bu yüzden bu klasörlerde kullanmadığınız asset’ler bırakmayın. Eğer projenize import ettiğiniz bir plugin’in içerisinde Resources klasörü varsa, klasörün içindeki asset’lerin plugin’in çalışması için gerekli olup olmadığını kontrol edin. Bazen plugin’in demo sahnesinde kullanılan asset’ler Resources klasöründe yer alır ve siz farkında olmadan oyununuza dahil olurlar.
- Shader’larınızdaki kullanılmayan özellikleri kapatın (shader stripping): https://yasirkula.com/2020/09/06/unity-shader-boyutu-optimizasyonu/
- Alpha kanalını kullanmadığınız texture’ların Inspector’daki Alpha Source‘unu None yapın. Bu şekilde bazı texture’ların boyutu yarıya kadar inebilir (zaten alpha’sı olmayan texture’lara bir etkisi olmaz). Alpha kanalı genelde saydamlık için kullanılır, bu yüzden sadece tamamen opak objelerin texture’ları bu optimizasyondan faydalanabilir. Nadiren de olsa alpha kanalı, Standard shader’da objenin ışıklandırmasına etki etmek için kullanılabilir. Bu yüzden bu optimizasyonu uygularken, sahnenizde objenin bir klonu olsun ve optimizasyonun objeye görsel olarak bir etkisi olmadığından emin olun.
- [4][14] 3D modellerinizin Mesh Compression‘ına Off harici bir değer vermeyi deneyin. Eğer 3D modelin görüntüsünde gözle görülür bir bozulma olursa bu ayarı geri alın.
- [14] Player Settings‘teki Vertex Compression, 3D modellerinizi sıkıştırma konusunda yardımcı olur. Burada Position harici her şeyi işaretlemek isteyebilirsiniz. Benzer şekilde, Optimize Mesh Data‘yı açarak, 3D modelde kullanılmayan UV kanalları vs. varsa bunların build esnasında modelden silinmesini sağlayabilirsiniz. Hangi kanalların kullanıldığını hesaplamak için, Unity modelin materyaline atanmış shader’ı inceler.
- (Android) Unity ile x86 işlemcili Android cihazlara da build almak mümkündür. Bu her ne kadar kulağa güzel gelse de, oyunun x86 işlemci versiyonunu APK’ya eklemek, dosya boyutunu ciddi miktarda artırabilir. x86 işlemciyi APK’nıza dahil etmemek için, Player Settings-Other Settings‘teki Device Filter‘ın değerini ARMv7 yapabilirsiniz. Zaten artık Google Play’e x86’lı bir APK’yı upload edemediğiniz için (Unity x64 desteklemediğinden), x86’yı APK’nıza dahil etmeniz için bir sebep kalmıyor. Unity’nin ilerleyen sürümleriyle birlikte x86 desteğinin tamamen kesileceğinin de buradan altını çizelim.
- (Mobil platformlar) Oyununuzu build alırken, kodunuzda kullandığınız class’ları içeren dll‘ler de apk dosyasına eklenir ancak bu dll’ler ile gelen kullanmadığınız diğer class’lar boş yere fazlalık oluşturur. Unity’nin build alırken bu class’ları otomatik olarak yok saymasını sağlayan bir özellik var: “Stripping Level” (Player Settings-Other Settings)(yeni Unity sürümlerinde “Managed Stripping Level“). Bu seçeneği “Strip Assemblies” yaparsanız dll dosyalarındaki fazlalıklar atılır (dokümantasyonda yazdığına göre Reflection kullanıyorsanız bu bir sıkıntı oluşturabilirmiş). Eğer bu seçeneği “Use micro mscorlib” yaparsanız “Strip Assemblies”e ilaveten bir de .NET kütüphanesinde de bir optimizasyon söz konusu olur (benim kullandığım seçenek bu). Bu saydığım seçenekler yeni Unity sürümlerinde Low, Medium ve High olarak geçmekte. Ben bu seçeneği High yapıyorum ve script’lerle alakalı bir sorun yaşamadığım sürece de High’da bırakıyorum.
- Window-Package Manager‘ı açıp All packages‘ı Built-in packages yapın ve oradaki ihtiyacınız olmayan modülleri Disable edin. Örneğin çoğu oyun AI (NavMesh), Asset Bundle veya Cloth modüllerini kullanmaz. Bazen kullandığınız bir plugin, kapattığınız bir modülü kullanıyor olabilir. Neyse ki bu durumda konsola bir hata mesajı düşer ve hangi modülü tekrar Enable ederek sorunu çözebileceğiniz yazar.
- [1] Edit-Project Settings-Graphics‘te “Built-in Shader Settings” başlığı altında yer alan kullanmadığınız grafik özelliklerinin değerini “Built-in shader“dan “No Support“a çekin. Örneğin çoğu mobil oyunda Deferred, Motion Vectors, Light Halo ve Lens Flare kullanılmaz. Benzer şekilde, Always Included Shaders‘ta bariz kullanmadığınız shader’lar varsa bunları kaldırın; örneğin Video Player kullanmıyorsanız Video shader’larını ve UI sistemini kullanmıyorsanız UI shader’larını kaldırabilirsiniz.
Diğer Optimizasyonlar
- [3] Sırf Hierarchy düzenli dursun diye objelerinizi Empty GameObject‘lerin child’ı yapmayın. Bir child objenin Transform’u değiştiğinde, Unity kendi içinde bu Transform’un tüm kardeşlerini potansiyel olarak değişmiş işaretler ve bu işlem CPU’dan yer. Bunun için sahnenizdeki hareketli objeler Hierarchy’nin root’unda dursun, yani bir parent’ları olmasın. Ama bir Empty GameObject’in içindeki tüm objeler hareketsizse, o Empty GameObject o şekilde kalabilir, bir sıkıntı yok.
- [5][9][16] Çok basit animasyonlar için Animator component’i yerine Animation component’ini kullanın veya DOTween gibi kod bazlı bir animasyon plugin’i kullanın.
- Çok kısa ve sıklıkla kullandığınız AudioClip‘lerin Load Type‘ını Decompress On Load yapın. Bu AudioClip’ler hafızada çok daha fazla yer kaplar ama oyun esnasında decompression için CPU harcamazlar. Bu yüzden de bu ayar sadece çok kısa ses efektleri için idealdir. Müziklerde ise Streaming seçeneğini seçin. Bu ayar seçili olduğunda müzik diskten okunur ve müziğin tamamı hafızaya alınmaz, sadece çalmakta olan kısmı hafızaya alınır.
- [14][16] 3D uzayda çalan AudioClip‘lerin Force To Mono seçeneğini açın; aksi taktirde bu sesler oyun esnasında dinamik olarak mono’ya çevrilirler ve bu esnada CPU harcarlar. Aslında müzik gibi, 3D uzaydan bağımsız olarak çalan ses dosyalarını da Force To Mono yapmak isteyebilirsiniz çünkü AudioSource component’i, Mono sesleri daha kolay çalar.
- AudioClip‘lerle ilgili diğer optimizasyonlar için: https://yasirkula.com/2020/03/31/unity-audioclip-import-ayarlari/
- [3] Oyununuzda aynı anda pek çok AudioSource oynuyorsa, Edit-Project Settings-Audio‘daki Max Virtual Voices ve Max Real Voices‘ın değerlerini olabildiğince kısarak, Unity’nin tek bir frame’de hesaba kattığı AudioSource sayısına bir limit koyabilirsiniz.
- (iOS) [17] Eğer projenizde Input.acceleration ve Input.gyro‘dan faydalanmıyorsanız, Player Settings‘teki Accelerometer Frequency‘nin değerini Disabled yaparak, bu sensörün boş yere CPU harcamasını engelleyin.
- Oyun boyunca sürekli kullandığınız asset’leri, Player Settings‘teki Preloaded Assets listesine ekleyerek, asset’in oyunun başında hafızaya alınıp oyun bitene kadar hafızadan çıkmamasını sağlayabilirsiniz. Bu sayede sahneler arası geçişlerde bile asset hafızada kalır ve oyun esnasında asset’i yüklerken herhangi bir gecikme yaşanmaz.
Yararlanılan Kaynaklar
[1] https://learn.unity.com/tutorial/optimizing-graphics-in-unity
[2] https://learn.unity.com/tutorial/optimizing-unity-ui
[3] Unite Berlin 2018 – Unity’s Evolving Best Practices: https://www.youtube.com/watch?v=W45-fsnPhJY
[4] https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity.html
[5] Unite Europe 2017 – Squeezing Unity: Tips for raising performance: https://www.youtube.com/watch?v=_wxitgdx-UI
[6] https://www.habrador.com/tutorials/unity-optimization/
[7] https://makaka.org/unity-tutorials/optimization
[8] Unite 2016 – Let’s Talk (Content) Optimization: https://www.youtube.com/watch?v=n-oZa4Fb12U
[9] https://unity3d.com/how-to/unity-best-practices-for-engine-performance
[10] Unite 2012 – Performance Optimization Tips and Tricks for Unity: https://www.youtube.com/watch?v=jZ4LL1LlqF8
[11] https://unity3d.com/how-to/work-optimally-with-unity-apis
[12] https://unity3d.com/how-to/unity-common-mistakes-to-avoid
[13] https://unity3d.com/how-to/unity-ui-optimization-tips
[14] https://learn.unity.com/tutorial/mobile-optimization
[15] https://learn.unity.com/tutorial/fixing-performance-problems
[16] https://create.unity3d.com/nine-ways-to-optimize-game-development
[17] Optimization tips for maximum performance – Part 1 | Unite Now 2020: https://www.youtube.com/watch?v=ZRDHEqy2uPI