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:
- İ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. - 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
veTextMeshProUGUI
dahil metinler -
UnityEngine.UI.InputField
veTMP_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:
ButtonRole.Settings
düğmesini tıklayınTextRole.SomeText
metnin "bazı değerlere" eşit olup olmadığını kontrol edinBelirli 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!