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.
Ö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.
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.
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.
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 .
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.
UnityEngine.UI.Text
ve TextMeshProUGUI
dahil metinlerUnityEngine.UI.InputField
ve TMP_InputField
dahil TextInput'lar
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); } }
Metinlerin bulunduğu sekme, rol ve nesne bağlantısına ek olarak aşağıdaki sütunları içerir:
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.
Ç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");
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.
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:
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.
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.
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)); }
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.
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.
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:
ButtonRole.Settings
düğmesini tıklayın
TextRole.SomeText
metnin "bazı değerlere" eşit olup olmadığını kontrol edin
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!