Ayrıntılı bağlantıda sık sık başvurulan Unity3D coroutines öldü. Yorumlarda ve cevaplarda bahsedildiği için yazının içeriğini burada yayınlayacağım. Bu içerik bu aynadan geliyor .
Ayrıntılı Unity3D coroutines
Oyunlarda pek çok işlem, birden çok çerçeve boyunca gerçekleşir. Her karede çok çalışan, ancak kare hızını çok fazla etkilememek için birden çok kareye bölünen yol bulma gibi 'yoğun' süreçleriniz var. Çoğu karede hiçbir şey yapmayan, ancak bazen kritik işler yapması için çağrılan oyun tetikleyicileri gibi 'seyrek' süreçleriniz var. Ve ikisi arasında çeşitli süreçler var.
Çok iş parçacığı olmadan birden çok çerçeve üzerinde gerçekleşecek bir işlem oluşturduğunuzda, işi kare başına bir çalıştırılabilen parçalara bölmenin bir yolunu bulmanız gerekir. Merkezi döngüye sahip herhangi bir algoritma için oldukça açıktır: Örneğin bir A * yol bulucu, denemek yerine her çerçevede açık listeden yalnızca bir avuç düğümü işleyerek, düğüm listelerini yarı kalıcı olarak koruyacak şekilde yapılandırılabilir. tüm işi tek seferde yapmak. Gecikmeyi yönetmek için yapılacak bir miktar dengeleme var - sonuçta, kare hızınızı saniyede 60 veya 30 kare ile kilitliyorsanız, işleminiz saniyede yalnızca 60 veya 30 adım atacaktır ve bu, sürecin sadece devam etmesine neden olabilir. genel olarak çok uzun. Düzgün bir tasarım, mümkün olan en küçük çalışma birimini bir seviyede sunabilir - örneğin tek bir A * düğümünü işleyin - ve üstteki katman, birlikte daha büyük parçalar halinde çalışmanın bir yoludur - örneğin, X milisaniye boyunca A * düğümlerini işlemeye devam edin. (Bazı insanlar buna 'zaman dilimleme' diyorlar, ben istemiyorum).
Yine de, çalışmanın bu şekilde bölünmesine izin vermek, durumu bir çerçeveden diğerine aktarmanız gerektiği anlamına gelir. Yinelemeli bir algoritmayı bozuyorsanız, yinelemeler arasında paylaşılan tüm durumu ve daha sonra hangi yinelemenin gerçekleştirileceğini izlemenin bir yolunu korumanız gerekir. Bu genellikle çok kötü değildir - 'A * yol bulucu sınıfının' tasarımı oldukça açıktır - ancak daha az hoş olan başka durumlar da vardır. Bazen çerçeveden çerçeveye farklı türde işler yapan uzun hesaplamalarla karşı karşıya kalırsınız; Durumlarını yakalayan nesne, verileri bir çerçeveden diğerine geçirmek için saklanan, yarı yararlı 'yerellerin' büyük bir karmaşasıyla sonuçlanabilir. Ve seyrek bir süreçle uğraşıyorsanız, genellikle işin ne zaman yapılması gerektiğini izlemek için küçük bir durum makinesi uygulamak zorunda kalırsınız.
Tüm bu durumu birden çok çerçevede açıkça izlemek zorunda kalmak yerine ve senkronizasyon ve kilitlemeyi çoklu iş parçacığı olarak okumak ve yönetmek zorunda kalmak yerine, işlevinizi tek bir kod parçası olarak yazmanız düzgün olmaz mıydı ve işlevin 'duraklaması' ve daha sonra devam etmesi gereken belirli yerleri işaretleyin?
Unity - bir dizi başka ortam ve dil ile birlikte - bunu Coroutines biçiminde sağlar.
Nasıl görünuyorlar? "Unityscript" (Javascript) içinde:
function LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield;
}
}
C # dilinde:
IEnumerator LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield return null;
}
}
Nasıl çalışırlar? Unity Technologies için çalışmadığımı hemen söylememe izin verin. Unity kaynak kodunu görmedim. Unity'nin coroutine motorunun cesaretini hiç görmedim. Ancak, tarif edeceğimden tamamen farklı bir şekilde uyguladılarsa, o zaman oldukça şaşıracağım. UT'den biri içeri girip aslında nasıl çalıştığı hakkında konuşmak isterse, bu harika olur.
Büyük ipuçları C # sürümündedir. İlk olarak, işlevin dönüş türünün IEnumerator olduğunu unutmayın. İkinci olarak, ifadelerden birinin getiri getirisi olduğuna dikkat edin. Bu, verim değerinin bir anahtar kelime olması gerektiği ve Unity'nin C # desteği vanilya C # 3.5 olduğu için vanilya C # 3.5 anahtar kelimesi olması gerektiği anlamına gelir. Aslında burada MSDN'de - 'yineleyici bloklar' denen bir şeyden bahsediyoruz. Yani, ne oluyor?
İlk olarak, bu IEnumerator türü var. IEnumerator türü, bir dizi üzerinde imleç gibi davranarak iki önemli üye sağlar: İmlecin o anda üzerinde olduğu öğeyi size veren bir özellik olan Current ve dizideki sonraki öğeye hareket eden bir işlev olan MoveNext (). IEnumerator bir arabirim olduğundan, bu üyelerin tam olarak nasıl uygulandığını belirtmez; MoveNext () yalnızca bir taneCurrent ekleyebilir veya yeni değeri bir dosyadan yükleyebilir veya internetten bir görüntü indirip hashing yapabilir ve yeni hash'i Current olarak depolayabilir ... veya hatta ilk için bir şey yapabilir Sıradaki öğe ve ikincisi için tamamen farklı bir şey. İsterseniz sonsuz bir dizi oluşturmak için bile kullanabilirsiniz. MoveNext (), dizideki sonraki değeri hesaplar (daha fazla değer yoksa yanlış döndürür),
Normalde, bir arabirim uygulamak istiyorsanız, bir sınıf yazmanız, üyeleri uygulamanız vb. Gerekir. Yineleyici blokları, IEnumerator'ü tüm bu güçlükler olmadan uygulamanın uygun bir yoludur - yalnızca birkaç kuralı takip edersiniz ve IEnumerator uygulaması derleyici tarafından otomatik olarak oluşturulur.
Yineleyici bloğu, (a) IEnumerator döndüren ve (b) getiri anahtar sözcüğünü kullanan normal bir işlevdir. Öyleyse getiri anahtar kelimesi gerçekte ne yapar? Sıradaki bir sonraki değerin ne olduğunu veya daha fazla değer olmadığını bildirir. Kodun bir getiri getirisi X veya getiri kesintisiyle karşılaştığı nokta, IEnumerator.MoveNext () 'in durması gereken noktadır; bir getiri dönüşü X, MoveNext () işlevinin true döndürmesine veCurrent değerinin X değerine atanmasına neden olurken, getiri kesintisi MoveNext () 'in false döndürmesine neden olur.
Şimdi, işte püf noktası. Dizi tarafından döndürülen gerçek değerlerin ne olduğu önemli olmak zorunda değildir. MoveNext () öğesini tekrar tekrar çağırabilir ve Current'ı yok sayabilirsiniz; hesaplamalar yine de yapılacaktır. MoveNext () her çağrıldığında, yineleyici bloğunuz, gerçekte hangi ifadeyi verdiğine bakılmaksızın bir sonraki 'verim' ifadesine çalışır. Böylece şöyle bir şey yazabilirsiniz:
IEnumerator TellMeASecret()
{
PlayAnimation("LeanInConspiratorially");
while(playingAnimation)
yield return null;
Say("I stole the cookie from the cookie jar!");
while(speaking)
yield return null;
PlayAnimation("LeanOutRelieved");
while(playingAnimation)
yield return null;
}
ve aslında yazdığınız şey, uzun bir boş değerler dizisi oluşturan bir yineleyici bloğudur, ancak önemli olan, onları hesaplamak için yaptığı işin yan etkileridir. Bu coroutini aşağıdaki gibi basit bir döngü kullanarak çalıştırabilirsiniz:
IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }
Veya daha kullanışlı bir şekilde, onu başka bir işle karıştırabilirsiniz:
IEnumerator e = TellMeASecret();
while(e.MoveNext())
{
// If they press 'Escape', skip the cutscene
if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}
Hepsi zamanlamada Gördüğünüz gibi, her bir getiri dönüş ifadesi bir ifade sağlamalıdır (null gibi), böylece yineleyici bloğun aslında IEnumerator.Current'a atayacağı bir şey vardır. Uzun bir sıfır dizisi tam olarak yararlı değildir, ancak yan etkilerle daha çok ilgileniyoruz. Değil miyiz?
Aslında bu ifadeyle yapabileceğimiz kullanışlı bir şey var. Ya sadece boş bırakmak ve onu görmezden gelmek yerine, daha fazla iş yapmamız gerektiğini umduğumuzda gösteren bir şey verdiysek? Çoğunlukla bir sonraki kareye devam etmemiz gerekir, elbette, ancak her zaman değil: Bir animasyon veya ses oynatıldıktan sonra veya belirli bir süre geçtikten sonra devam etmek istediğimiz pek çok zaman olacaktır. While (playingAnimation), null döndürür; yapılar biraz sıkıcı, sence de öyle değil mi?
Unity, YieldInstruction temel türünü bildirir ve belirli bekleme türlerini gösteren birkaç somut türetilmiş tür sağlar. Beklenen süre geçtikten sonra coroutine devam eden WaitForSeconds'a sahipsiniz. Aynı çerçevede daha sonra belirli bir noktada koroutini devam ettiren WaitForEndOfFrame'e sahipsiniz. Coroutine türünün kendisine sahipsiniz, bu, coroutine A coroutine B'yi verdiğinde, coroutine A'yı coroutine B bitene kadar duraklatır.
Çalışma zamanı açısından bu neye benziyor? Dediğim gibi, Unity için çalışmıyorum, bu yüzden kodlarını hiç görmedim; ama biraz şöyle görünebileceğini tahmin ediyorum:
List<IEnumerator> unblockedCoroutines;
List<IEnumerator> shouldRunNextFrame;
List<IEnumerator> shouldRunAtEndOfFrame;
SortedList<float, IEnumerator> shouldRunAfterTimes;
foreach(IEnumerator coroutine in unblockedCoroutines)
{
if(!coroutine.MoveNext())
// This coroutine has finished
continue;
if(!coroutine.Current is YieldInstruction)
{
// This coroutine yielded null, or some other value we don't understand; run it next frame.
shouldRunNextFrame.Add(coroutine);
continue;
}
if(coroutine.Current is WaitForSeconds)
{
WaitForSeconds wait = (WaitForSeconds)coroutine.Current;
shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine);
}
else if(coroutine.Current is WaitForEndOfFrame)
{
shouldRunAtEndOfFrame.Add(coroutine);
}
else /* similar stuff for other YieldInstruction subtypes */
}
unblockedCoroutines = shouldRunNextFrame;
Diğer durumları ele almak için nasıl daha fazla YieldInstruction alt tipinin eklenebileceğini hayal etmek zor değil - örneğin, sinyaller için motor seviyesinde destek, onu destekleyen bir WaitForSignal ("SignalName") YieldInstruction ile eklenebilir. Daha fazla YieldInstructions ekleyerek, coroutinlerin kendileri daha anlamlı hale gelebilir - get return new WaitForSignal ("GameOver") bir süredir okunması daha güzeldir (! Signals.HasFired ("GameOver")), bana sorarsanız, tamamen farklı olarak boş döndürür. motorda yapmanın komut dosyasında yapmaktan daha hızlı olabileceği gerçeği.
Bariz olmayan birkaç sonuç Tüm bunlarla ilgili, insanların bazen gözden kaçırdığı ve benim de bahsetmem gerektiğini düşündüğüm birkaç yararlı şey var.
İlk olarak, verim dönüşü sadece bir ifade - herhangi bir ifade - ve YieldInstruction normal bir türdür. Bu, aşağıdaki gibi şeyler yapabileceğiniz anlamına gelir:
YieldInstruction y;
if(something)
y = null;
else if(somethingElse)
y = new WaitForEndOfFrame();
else
y = new WaitForSeconds(1.0f);
yield return y;
Belirli satırlar, yeni WaitForSeconds () döndürür, döndürür yeni WaitForEndOfFrame () vb. Döndürür, yaygındır, ancak aslında kendi başlarına özel formlar değildir.
İkincisi, bu eşgüdüm blokları sadece yineleyici bloklar olduğu için, isterseniz bunları kendiniz yineleyebilirsiniz - bunu sizin için motora yaptırmanız gerekmez. Bunu daha önce bir coroutine kesme koşulları eklemek için kullandım:
IEnumerator DoSomething()
{
/* ... */
}
IEnumerator DoSomethingUnlessInterrupted()
{
IEnumerator e = DoSomething();
bool interrupted = false;
while(!interrupted)
{
e.MoveNext();
yield return e.Current;
interrupted = HasBeenInterrupted();
}
}
Üçüncüsü, diğer coroutine'lerde verebildiğiniz gerçeği, kendi YieldInstructions'ınızı, motor tarafından uygulanmış gibi yüksek performanslı olmasa da uygulamanıza izin verebilir. Örneğin:
IEnumerator UntilTrueCoroutine(Func fn)
{
while(!fn()) yield return null;
}
Coroutine UntilTrue(Func fn)
{
return StartCoroutine(UntilTrueCoroutine(fn));
}
IEnumerator SomeTask()
{
/* ... */
yield return UntilTrue(() => _lives < 3);
/* ... */
}
ancak bunu gerçekten tavsiye etmem - bir Coroutine başlatmanın maliyeti benim beğenime göre biraz ağır.
Sonuç Umarım bu, Unity'de bir Coroutine kullandığınızda gerçekte neler olduğunu biraz açıklığa kavuşturur. C # 'ın yineleyici blokları harika küçük bir yapıdır ve Unity kullanmıyor olsanız bile, belki onlardan aynı şekilde yararlanmayı faydalı bulabilirsiniz.
IEnumerator
/IEnumerable
(veya genel eşdeğerleri) döndüren veyield
anahtar kelimeyi içeren yöntemleri dönüştürür . Yineleyicileri arayın.