paint-brush
StatefulUI: Durumlara ve İşaretlemeye Dayalı Bir Birlik Kullanıcı Arayüzü Kitaplığıile@dmitrii
2,926 okumalar
2,926 okumalar

StatefulUI: Durumlara ve İşaretlemeye Dayalı Bir Birlik Kullanıcı Arayüzü Kitaplığı

ile Dmitrii Ivashchenko12m2023/05/17
Read on Terminal Reader
Read this story w/o Javascript

Çok uzun; Okumak

Dmitrii Ivashchenko, MY.GAMES'te Yazılım Mühendisidir. Bu yazımızda Unity'de elementlerin durumları ve işaretlemelerine dayalı bir kullanıcı arayüzü geliştirmekten bahsedeceğiz. Açıklanan yaklaşım [UI Toolkit] veya diğer UI oluşturma sistemleri için geçerli değildir.

People Mentioned

Mention Thumbnail
featured image - StatefulUI: Durumlara ve İşaretlemeye Dayalı Bir Birlik Kullanıcı Arayüzü Kitaplığı
Dmitrii Ivashchenko HackerNoon profile picture
0-item
1-item

Herkese merhaba, adım Dmitrii Ivashchenko ve MY.GAMES'te Yazılım Mühendisiyim. Bu yazımızda Unity'de elementlerin durumları ve işaretlemelerine dayalı bir kullanıcı arayüzü geliştirmekten bahsedeceğiz.

Giriş

Öncelikle şunu belirtelim ki, belgelere göre Runtime için hala tavsiye edilen Unity UI (uGUI) teknolojisi bağlamında konuşacağız. Açıklanan yaklaşım UI Araç Seti , IMGUI veya diğer UI oluşturma sistemleri için geçerli değildir.



Unity projelerinde çoğunlukla MonoBehaviour devralınan View sınıfları üzerine kurulu ve çok sayıda SerializeField alanıyla süslenmiş kullanıcı arayüzü uygulamalarıyla karşılaşırsınız. Bu yaklaşım, kullanıcı arayüzünün davranışı üzerinde tam kontrol sağlar, ancak aynı zamanda Görünüm ve Sunucu düzeylerinde (kullanılan mimariye bağlı olarak) büyük miktarda kod yazmayı da gerekli kılar.


Çoğu zaman, proje geliştirme devam ettikçe bu sınıflar inanılmaz boyutlara ulaşır ve GameObject'teki bileşenler dahili nesnelere giden çok sayıda bağlantıyla kaplanır:



Bileşenleri bu şekilde değiştirmek de eğlenceli değil: Bir sınıftaki yeni bir öğeye referans almak için SerializeField eklemeniz, kodu yeniden derlemeniz, prefabrik bileşende yeni alanı bulmanız ve gerekli nesneyi bu alana sürüklemeniz gerekir. Proje büyüdükçe derleme süresi, alan sayısı ve prefabrik yapıları düzenlemenin karmaşıklığı da artar.


Sonuç olarak, MonoBehaviour büyük ve aşırı yüklenmiş alt sınıflarıyla (veya tercihinize bağlı olarak çok sayıda küçük alt sınıfla) karşılaşırız.


Böyle bir kullanıcı arayüzünün davranışında yapılacak herhangi bir değişikliğin programcıya ait bir görev olduğunu ve bu görevin ilgili tüm maliyetleri de beraberinde getirdiğini dikkate almak gerekir: kod incelemesi, birleştirme çatışmalarını çözme, testlerle kod kapsamı vb.


Birden fazla duruma sahip pencerelerin uygulanmasını vurgulamak istiyorum. İki yaklaşıma ayrılabilecek birçok varyasyon gördüm:


  1. İlk olarak, pencere durumundaki herhangi bir değişiklik code kullanılarak gerçekleşir. Metnin rengini değiştirmek, bir görüntüyü değiştirmek, bir animasyonu oynatmak, bir nesneyi ekranda taşımak için; ilgili tüm nesneler ve parametreler karşılık gelen bir SerializeField gerektirir ve ardından gereksinimlere göre çalışmasını sağlamak için büyük miktarda kod yazılır. . Doğal olarak, yalnızca bir programcı bunun üstesinden gelebilir ve uygulamanın uzun, pahalı ve süper verimli (çoğunlukla herkesin fark edebileceğinden çok daha verimli) olduğu ortaya çıkar.
  2. Bir diğer yaklaşım ise “ çok güçlü Animatör ” olarak tanımlanabilir. View sınıfına ek olarak bir Animatör Denetleyicisi oluşturulur ve parametreler aracılığıyla kontrol edilir. Yeni pencerede yeni bir Animatör belirir ve pencereler görüntülenirken FPS düşmeye başlayana kadar bu şekilde devam eder.



Artık uGUI ile çalışmanın bazı zorluklarını vurguladığımıza göre, bu sorunun çözümüne yönelik farklı bir yaklaşımdan bahsetmek istiyorum.

Durum bilgisi olan kullanıcı arayüzü

Evcil hayvan projelerimden biri üzerinde çalışırken, Unity'de yapılandırılmış kullanıcı arayüzü geliştirmeye yönelik bir kütüphane geliştirdim. Daha sonra ekibim ve ben bunu üretimde test ettik ve sonuçlardan memnun kaldık.


Kütüphanenin kaynak kodu GitHub'dan indirilebilir .

Durum Bilgili Bileşen

Kütüphanenin temel öğesi StatefulComponent bileşenidir. Bu bileşen, her ekranın kök GameObject'ine yerleştirilir ve sekmeler arasında dağıtılmış şekilde dahili öğelere ilişkin gerekli tüm referansları içerir:



Her bağlantı rolüne göre adlandırılır. Kod açısından bakıldığında roller kümesi düzenli bir enum . Her kullanıcı arayüzü öğesi türü (düğmeler, resimler, metinler vb.) için ayrı rol kümeleri hazırlanır:


 public enum ButtonRole { ... } public enum ImageRole { ... } public enum TextRole { ... } ...


Roller doğrudan bileşenden oluşturulur ve enum manuel olarak düzenlenmesine gerek yoktur. Bir rol oluştururken yeniden derlemeyi beklemek de gerekli değildir, çünkü bu enum öğeleri oluşturulduktan hemen sonra kullanılabilir.


Birleştirme çakışmalarını basitleştirmek için numaralandırma değerleri, öğelerin adlarına göre hesaplanır:


 [StatefulUI.Runtime.RoleAttributes.ButtonRoleAttribute] public enum ButtonRole { Unknown = 0, Start = -1436209294, // -1436209294 == "Start".GetHashCode() Settings = 681682073, Close = -1261564850, Quests = 1375430261, }


Bu, siz ve iş arkadaşlarınızın farklı dallardaki düğmeler için aynı anda yeni roller oluşturmanız durumunda, prefabriklerdeki serileştirilmiş değerlerin bozulmasını önlemenize olanak tanır.


Her kullanıcı arayüzü öğesi türü (düğmeler, metinler, resimler) kendi sekmesinde bulunur:



Rolleri kullanarak prefabrik içindeki tüm öğelerin tam olarak işaretlenmesi sağlanır. Resimlere ve metinlere erişmek için artık SerializeField kümelerine gerek yoktur ve örneğin hareketli grafiği değiştirmek için StatefulComponent tek bir referansa sahip olmak ve istenen görüntünün rolünü bilmek yeterlidir.


Şu anda erişilebilen öğe türleri şunlardır:


  • Düğmeler, Görseller, Geçişler, Kaydırıcılar, Açılır Menüler, Video Oynatıcılar, Animatörler
  • UnityEngine.UI.Text ve TextMeshProUGUI dahil metinler
  • UnityEngine.UI.InputField ve TMP_InputField dahil TextInput'lar
  • Nesneler — isteğe bağlı nesnelere yapılan başvurular için.


Açıklamalı nesnelerle çalışmak için ilgili yöntemler vardır. Kodda StatefulComponent referansını kullanabilir veya sınıfı StatefulView devralabilirsiniz:


 public class ExamplePresenter { private StatefulComponent _view; public void OnOpen() { _view.GetButton(ButtonRole.Settings).onClick.AddListener(OnSettingsClicked); _view.GetButton(ButtonRole.Close).onClick.AddListener(OnCloseClicked); _view.GetSlider(SliderRole.Volume).onValueChanged.AddListener(OnVolumeChanged); } } public class ExampleScreen : StatefulView { private void Start() { SetText(TextRole.Title, "Hello World"); SetTextValues(TextRole.Timer, hours, minutes, seconds); SetImage(ImageRole.UserAvatar, avatarSprite); } }

Metinler ve Yerelleştirme

Metinlerin bulunduğu sekme, rol ve nesne bağlantısına ek olarak aşağıdaki sütunları içerir:


  • Kod: yerelleştirme için bir metin anahtarı
  • Yerelleştir onay kutusu: metin alanının yerelleştirmeye tabi olduğunu gösteren bir gösterge
  • Değer: nesnenin geçerli metin içeriği
  • Yerelleştirilmiş: Kod alanındaki anahtarla bulunan mevcut metin



Kütüphane, çevirilerle çalışmak için yerleşik bir alt sistem içermez. Yerelleştirme sisteminizi bağlamak için ILocalizationProvider arayüzünün bir uygulamasını oluşturmanız gerekir. Bu, örneğin Arka Uç, ScriptableObjects veya Google E-Tablolarınıza dayalı olarak oluşturulabilir.


 public class HardcodeLocalizationProvider : ILocalizationProvider { private Dictionary<string, string> _dictionary = new Dictionary<string, string> { { "timer" , "{0}h {1}m {2}s" }, { "title" , "Título do Jogo" }, { "description" , "Descrição longa do jogo" }, }; public string GetPhrase(string key, string defaultValue) { return _dictionary.TryGetValue(key, out var value) ? value : defaultValue; } }


Yerelleştirmeyi Kopyala butonuna tıkladığınızda Kod ve Değer sütunlarının içerikleri Google E-Tablolar'a yapıştırmaya uygun formatta panoya kopyalanacaktır.

Dahili Bileşenler

Çoğu zaman, yeniden kullanımı kolaylaştırmak için kullanıcı arayüzünün ayrı bölümleri ayrı prefabrik yapılara çıkarılır. StatefulComponent ayrıca her bileşenin yalnızca kendi alt arayüz öğeleriyle çalıştığı bir bileşenler hiyerarşisi oluşturmamıza da olanak tanır.


İç Kompozisyonlar sekmesinde dahili bileşenlere roller atayabilirsiniz:



Yapılandırılmış roller kodda diğer öğe türlerine benzer şekilde kullanılabilir:


 var header = GetInnerComponent(InnerComponentRole.Header); header.GetButton(ButtonRole.Close).onClick.AddListener(OnCloseClicked); header.SetText(TextRole.Title, "Header Title"); var footer = GetInnerComponent(InnerComponentRole.Footer); footer.GetButton(ButtonRole.Continue).onClick.AddListener(OnContinueClicked); footer.SetText(TextRole.Message, "Footer Message");

Konteynerler

Benzer öğelerin bir listesini oluşturmak için ContainerView bileşenini kullanabilirsiniz. Örnekleme için hazır yapıyı ve kök nesneyi (isteğe bağlı) belirtmeniz gerekir. Düzenleme modunda, StatefulComponent kullanarak öğe ekleyebilir ve kaldırabilirsiniz:



Örneklendirilmiş prefabrik yapıların içeriğini işaretlemek için StatefulComponent kullanmak uygundur. Çalışma zamanında, kapsayıcıyı doldurmak için AddInstance<T> , AddStatefulComponent veya FillWithItems yöntemlerini kullanabilirsiniz:


 var container = GetContainer(ContainerRole.Players); container.Clear(); container.FillWithItems(_player, (StatefulComponent view, PlayerData data) => { view.SetText(TextRole.Name, data.Name); view.SetText(TextRole.Level, data.Level); view.SetImage(ImageRole.Avatar, data.Avatar); });


Eğer standart Object.Instantiate() nesne oluşturmak için size uygun değilse, bu davranışı örneğin Zenject kullanarak örnekleme için geçersiz kılabilirsiniz:


 StatefulUiManager.Instance.CustomInstantiateMethod = prefab => { return _diContainer.InstantiatePrefab(prefab); };


Dahili Bileşenler ve Kapsayıcılar, StatefulComponent için sırasıyla statik ve dinamik iç içe yerleştirme sağlar.


Prefabriklerin işaretlenmesini, yerelleştirilmesini ve örneklemeyi değerlendirdik. Şimdi en ilginç kısma geçmenin zamanı geldi; durumlara dayalı kullanıcı arayüzleri geliştirmeye.

Devletler

Devlet kavramını bir prefabrik yapıdaki adlandırılmış değişiklikler dizisi olarak ele alacağız. Bu durumda ad, StateRole numaralandırmasındaki bir roldür ve prefabrikteki değişiklik örnekleri şunlar olabilir:


  • GameObject'i etkinleştirme ve devre dışı bırakma
  • Görüntü nesneleri için spriteları veya malzemeleri değiştirme
  • Ekrandaki nesneleri taşıma
  • Metinleri ve görünümlerini değiştirme
  • Animasyonları oynatma
  • Ve benzeri — kendi nesne manipülasyonu türlerinizi ekleyebilirsiniz


Durumlar sekmesinde bir dizi değişiklik (Durum Açıklaması) yapılandırılabilir. Yapılandırılmış bir durum doğrudan denetçiden uygulanabilir:



Yapılandırılmış bir durum, SetState yöntemi kullanılarak koddan uygulanabilir:


 switch (colorScheme) { case ColorScheme.Orange: SetState(StateRole.Orange); break; case ColorScheme.Red: SetState(StateRole.Red); break; case ColorScheme.Purple: SetState(StateRole.Purple); break; }


Araçlar sekmesinde, Etkinleştirildiğinde Başlangıç Durumunu Uygula parametresi etkinleştirildiğinde, nesne örneği oluşturulduktan hemen sonra uygulanacak Durumu yapılandırabilirsiniz.


Durumların kullanılması, View sınıfı düzeyinde gereken kod miktarında önemli bir azalmaya olanak tanır. Ekranınızın her durumunu StatefulComponent bir dizi değişiklik olarak tanımlayın ve oyunun durumuna bağlı olarak koddan gerekli Durumu uygulayın.

Devlet Ağacı

Aslında durumlara dayalı bir kullanıcı arayüzü geliştirmek inanılmaz derecede kullanışlıdır. Öyle ki, zamanla başka bir soruna yol açar; proje geliştikçe, tek bir pencereye ilişkin durum listesi aşırı uzunluğa ulaşabilir ve bu nedenle gezinmek zorlaşır. Ayrıca, yalnızca diğer bazı durumların bağlamında anlamlı olan durumlar da vardır. Bu sorunu çözmek için Statful UI'nın başka bir aracı vardır: Durum Ağacı. Durumlar sekmesindeki Durum Ağacı Düzenleyicisi düğmesine tıklayarak buna erişebilirsiniz.


Bir sandık için ödül penceresi oluşturmamız gerektiğini varsayalım. Pencerenin 3 aşaması vardır:


  • Sandık animasyonlu tanıtımı ( Giriş durumu)
  • Sandıktan üç farklı ödül türünün döngüsel görünümü ( Ödül durumuna bağlı olarak Para , Emoji ve Kartlar belirtilir; bu, sandıktan görünen ödülün animasyonunu tetikler)
  • Verilen tüm ödüllerin tek bir listede görüntülenmesi ( Sonuçları belirtin)


Ana durumlar (bu örnekte Reward ), alt durumlar her çağrıldığında uygulanır:



Yapılandırılmış bir StatefulComponent yönetmek, bileşenleri gerekli verilerle dolduran ve durumları doğru anda değiştiren minimum miktarda basit ve anlaşılır koddan oluşur:


 public void ShowIntro() { SetState(StateRole.Intro); } public void ShowReward(IReward reward) { // Update the inner view with the reward reward.UpdateView(GetInnerComponent(InnerComponentRole.Reward)); // Switch on the type of reward switch (reward) { case ICardsReward cardsReward: SetState(StateRole.Cards); break; case IMoneyReward moneyReward: SetState(StateRole.Money); break; case IEmojiReward emojiReward: SetState(StateRole.Emoji); break; } } public void ShowResults(IEnumerable<IReward> rewards) { SetState(StateRole.Results); // Fill the container with the rewards GetContainer(ContainerRole.TotalReward) .FillWithItems(rewards, (view, reward) => reward.UpdateView(view)); }

Durum Bilgili API ve Dokümantasyon

Rollerin amacı, daha sonra kodda kullanılmak üzere bağlantıları ve durumları adlandırmak için kullanışlı ve net bir yol sağlamaktır. Bununla birlikte, bir durumu tanımlamanın çok uzun bir ad gerektirdiği durumlar vardır ve bu bağlantının neye işaret ettiği veya durumun hangi davranışı yansıttığı hakkında küçük bir yorum bırakmak daha uygun olacaktır. Bu gibi durumlarda, StatefulComponent her bağlantı ve durum bir açıklama eklemenize olanak tanır:



Her sekmede API Kopyala ve Belgeleri Kopyala düğmelerini zaten fark etmiş olabilirsiniz; bunlar, seçilen bölüm için kopyalama bilgileridir. Bunlara ek olarak, Araçlar sekmesinde benzer düğmeler vardır; bunlar tüm bölümler için bilgileri aynı anda kopyalar. API'yi Kopyala düğmesini tıkladığınızda, bu StatfulComponent nesnesini yönetmek için oluşturulan kod panoya kopyalanacaktır. Ödül penceremize bir örnek:


 // Insert the name of the chest here SetText(TextRole.Title, "Lootbox"); // Button to proceed to the reward issuance phase GetButton(ButtonRole.Overlay); // Button to display information about the card GetButton(ButtonRole.Info); // Container for displaying the complete list of awarded rewards GetContainer(ContainerRole.TotalReward); // Insert the card image here SetImage(ImageRole.Avatar, null); // Animated appearance of a chest SetState(StateRole.Intro);


Belgeleri Kopyala düğmesini tıkladığınızda, bu prefabrik yapının belgeleri Markdown formatında panoya kopyalanacaktır:


 ### RewardScreen Buttons: - Overlay - Button to proceed to the reward issuance phase - Info - Button to display information about the card Texts: - Title - Insert the name of the chest here Containers: - TotalReward - Container for displaying the complete list of awarded rewards Images: - Avatar - Insert the card image here States: - Intro - Animated appearance of a chest - Cards - Displaying rewards in the form of a card - Money - Displaying rewards in the form of game currency - Emoji - Displaying rewards in the form of an emoji - Results - Displaying a complete list of issued rewards


Bu kadar detaylı talimatların yer aldığı bu ekranı uygularken hata yapmanın oldukça zor olduğu aşikar. Projenin bilgi tabanında UI organizasyonunuzla ilgili güncel bilgileri kolayca tutabilirsiniz.


Aynı zamanda Durum Bilgili Kullanıcı Arayüzü, kullanıcı arayüzü prefabriklerinin oluşturulması için yetki verilmesine olanak tanır. Aslında durum tabanlı işaretleme, prefabrik yapının davranışını programcılara aktarmadan önce tamamen test etmeye olanak tanır. Bu, oyun tasarımcılarının , teknik tasarımcıların ve hatta ayrı kullanıcı arayüzü geliştiricilerinin prefabrikler hazırlayabileceği anlamına gelir. Ayrıca, kod ile prefabrik arasında bir API oluşturulduğundan, prefabriklerin programlanması ve yapılandırılması paralel olarak yapılabilir! Gerekli olan tek şey API'yi önceden formüle etmektir. Ancak prefabrik yapıları yapılandırma görevi programcılara kalsa bile Durum Bilgili Kullanıcı Arayüzünün kullanılması bu işi önemli ölçüde hızlandırır.

Çözüm

Gördüğümüz gibi Durum Bilgili Kullanıcı Arayüzü, kullanıcı arayüzü öğesi durumlarıyla çalışmayı önemli ölçüde basitleştirir. SerializeFields oluşturmak, kodu yeniden derlemek ve çok sayıda View sınıfı alanı arasında referans aramak için artık uzun döngülere gerek yok. View sınıflarının kendisinde, nesneleri açıp kapatmak veya metin rengini değiştirmek gibi tekrarlanan işlemler için artık büyük miktarda kod yazmaya gerek yok.


Kitaplık, bir projedeki düzenleri organize etme, prefabrik yapılar içindeki nesneleri işaretleme, durumlar oluşturma, bunları kullanıcı arayüzü öğelerine bağlama ve kullanıcı arayüzü yönetimi için bir API ve dokümantasyon sağlama konusunda tutarlı bir yaklaşıma olanak tanır. Ayrıca kullanıcı arayüzü prefabriklerinin oluşturulmasının devredilmesine olanak tanır ve onlarla çalışmayı hızlandırır.


İleriye dönük olarak proje yol haritası aşağıdaki öğeleri içermektedir:


  • Durumların yeteneklerini genişletme, Açıklamada yeni animasyon türleri, durumlarda ses çalma vb. gibi yeni türdeki kullanıcı arayüzü değişikliklerini destekleme


  • Metin ve resimleri renklendirmek için renk paletleri desteği ekleme


  • GameObjects'in yeniden kullanımıyla öğe listeleri için destek ekleme


  • Daha fazla sayıda Unity UI öğesinin desteklenmesi


  • Yerelleştirme için eklenen metinlerin kaldırılmasının otomatikleştirilmesi


  • Bir Test Çerçevesinin Uygulanması. Prefabriklerimizin kapsamlı işaretlemesine sahip olduğumuzdan, kurulumu kolay ScriptableObject tabanlı senaryoları aşağıdaki formatta oluşturabiliriz:


    1. ButtonRole.Settings düğmesini tıklayın

    2. TextRole.SomeText metnin "bazı değerlere" eşit olup olmadığını kontrol edin

    3. Belirli bir karaktere eşit olduğundan emin olmak için ImageRole.SomeImage görüntüyü kontrol edin


  • Bir eğitim sistemi. Teste benzer şekilde, işaretli düzen, " ButtonRole.UpgradeHero düğmesinde işaretçiyi göster" gibi talimatlar biçiminde ScriptableObject tabanlı eğitim senaryoları oluşturmaya olanak tanır.


Proje kaynak kodu GitHub'da mevcuttur . Sayılar oluşturabilir veya kütüphaneye katkıda bulunabilirsiniz!