paint-brush
StatefulUI: Thư viện giao diện người dùng thống nhất dựa trên trạng thái và đánh dấutừ tác giả@dmitrii
2,926 lượt đọc
2,926 lượt đọc

StatefulUI: Thư viện giao diện người dùng thống nhất dựa trên trạng thái và đánh dấu

từ tác giả Dmitrii Ivashchenko12m2023/05/17
Read on Terminal Reader

dài quá đọc không nổi

Dmitrii Ivashchenko là Kỹ sư phần mềm tại MY.GAMES. Trong bài viết này, chúng ta sẽ nói về việc phát triển giao diện người dùng trong Unity dựa trên trạng thái và đánh dấu các phần tử. Cách tiếp cận được mô tả không áp dụng cho [Bộ công cụ giao diện người dùng] hoặc các hệ thống xây dựng giao diện người dùng khác.
featured image - StatefulUI: Thư viện giao diện người dùng thống nhất dựa trên trạng thái và đánh dấu
Dmitrii Ivashchenko HackerNoon profile picture
0-item
1-item

Xin chào mọi người, tên tôi là Dmitrii Ivashchenko và tôi là Kỹ sư phần mềm tại MY.GAMES. Trong bài viết này, chúng ta sẽ nói về việc phát triển giao diện người dùng trong Unity dựa trên trạng thái và đánh dấu các phần tử.

giới thiệu

Trước hết, cần lưu ý rằng chúng ta sẽ nói về công nghệ Giao diện người dùng thống nhất (uGUI), công nghệ này vẫn được khuyến nghị cho Thời gian chạy theo tài liệu . Cách tiếp cận được mô tả không áp dụng cho UI Toolkit , IMGUI hoặc các hệ thống xây dựng UI khác.



Thông thường nhất trong các dự án Unity , bạn sẽ bắt gặp việc triển khai giao diện người dùng được xây dựng trên các lớp View được kế thừa từ MonoBehaviour và được thêm vào một số lượng lớn các trường SerializeField . Cách tiếp cận này cung cấp toàn quyền kiểm soát hành vi của giao diện người dùng, nhưng nó cũng khiến bạn phải viết một lượng lớn mã ở cấp độ Chế độ xem và Người trình bày (tùy thuộc vào kiến trúc được sử dụng).


Thông thường, khi quá trình phát triển dự án tiếp tục, các lớp này tăng lên với kích thước đáng kinh ngạc và bản thân các thành phần trên GameObject được bao phủ bởi một số lượng lớn các liên kết đến các đối tượng bên trong:



Việc sửa đổi các thành phần như thế này cũng không thú vị: để tham chiếu đến một thành phần mới trong một lớp, bạn cần thêm SerializeField , biên dịch lại mã, tìm trường mới trong thành phần prefab và kéo đối tượng cần thiết vào đó. Khi dự án phát triển, thời gian biên dịch, số lượng trường và độ phức tạp của việc tổ chức các prefab cũng lần lượt tăng lên.


Kết quả là, chúng tôi kết thúc với các lớp con MonoBehaviour cồng kềnh và quá tải (hoặc một số lượng lớn các lớp nhỏ, tùy thuộc vào sở thích của bạn).


Cũng đáng xem xét rằng bất kỳ thay đổi nào đối với hành vi của một giao diện người dùng như vậy đều là nhiệm vụ của lập trình viên và nhiệm vụ đó đi kèm với tất cả các chi phí liên quan: xem xét mã, giải quyết xung đột hợp nhất, phạm vi mã với các bài kiểm tra, v.v.


Tôi muốn làm nổi bật việc triển khai các cửa sổ có nhiều trạng thái. Tôi đã thấy nhiều biến thể, có thể được chia thành hai cách tiếp cận:


  1. Đầu tiên, bất kỳ thay đổi nào trong trạng thái cửa sổ đều xảy ra khi sử dụng mã . Để thay đổi màu của văn bản, thay đổi hình ảnh, phát hoạt ảnh, di chuyển một đối tượng trên màn hình — tất cả các đối tượng và tham số liên quan đều yêu cầu một SerializeField tương ứng, sau đó một lượng lớn mã được viết để làm cho nó hoạt động theo yêu cầu . Đương nhiên, chỉ có một lập trình viên mới có thể xử lý việc này và việc triển khai kéo dài, tốn kém và cực kỳ hiệu quả (thường hiệu quả hơn nhiều so với bất kỳ ai có thể nhận thấy).
  2. Một cách tiếp cận khác có thể được mô tả là “ Animator toàn năng ”. Ngoài lớp View, một Animator Controller được tạo và điều khiển thông qua các tham số. Một Animator mới xuất hiện trong cửa sổ mới, v.v., cho đến khi FPS khi hiển thị cửa sổ bắt đầu giảm.



Bây giờ chúng ta đã nhấn mạnh một số khó khăn khi làm việc với uGUI, tôi muốn nói về một cách tiếp cận khác để giải quyết vấn đề này.

giao diện người dùng trạng thái

Trong quá trình làm việc với một trong những dự án thú cưng của mình, tôi đã phát triển một thư viện để phát triển giao diện người dùng có cấu trúc trong Unity. Sau đó, nhóm của tôi và tôi đã thử nghiệm nó trên sản xuất và chúng tôi hài lòng với kết quả.


Mã nguồn của thư viện có sẵn để tải xuống trên GitHub .

Thành phần trạng thái

Yếu tố chính của thư viện là thành phần StatefulComponent . Thành phần này được đặt trên GameObject gốc của mỗi màn hình và chứa tất cả các tham chiếu cần thiết đến các thành phần bên trong, được phân phối trên các tab:



Mỗi liên kết được đặt tên dựa trên vai trò của nó. Từ góc độ mã, tập hợp các vai trò là một enum thông thường. Các bộ vai trò riêng biệt được chuẩn bị cho từng loại phần tử giao diện người dùng (nút, hình ảnh, văn bản, v.v.):


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


Các vai trò được tạo trực tiếp từ thành phần và không cần phải chỉnh sửa enum theo cách thủ công. Chờ biên dịch lại khi tạo vai trò cũng không cần thiết, vì các phần tử enum này có thể được sử dụng ngay sau khi tạo.


Để đơn giản hóa các xung đột hợp nhất, các giá trị liệt kê được tính toán dựa trên tên của các phần tử:


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


Điều này cho phép bạn tránh phá vỡ các giá trị được tuần tự hóa trong prefabs nếu bạn và đồng nghiệp của mình đồng thời tạo vai trò mới cho các nút trong các nhánh khác nhau.


Mỗi loại phần tử giao diện người dùng (nút, văn bản, hình ảnh) nằm trên tab riêng của nó:



Bằng cách sử dụng các vai trò, việc đánh dấu hoàn chỉnh tất cả các phần tử bên trong nhà lắp ghép đã đạt được. Các bộ SerializeField không còn cần thiết để truy cập hình ảnh và văn bản, và chỉ cần có một tham chiếu đến StatefulComponent và biết vai trò của hình ảnh mong muốn để thay thế sprite của nó chẳng hạn.


Các loại phần tử hiện có thể truy cập được là:


  • Nút, Hình ảnh, Chuyển đổi, Thanh trượt, Danh sách thả xuống, Trình phát video, Hoạt hình
  • Văn bản, bao gồm UnityEngine.UI.TextTextMeshProUGUI
  • TextInputs, bao gồm UnityEngine.UI.InputFieldTMP_InputField
  • Đối tượng - để tham chiếu đến các đối tượng tùy ý.


Có các phương thức tương ứng để làm việc với các đối tượng được chú thích. Trong mã, bạn có thể sử dụng tham chiếu đến StatefulComponent hoặc kế thừa lớp từ StatefulView :


 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); } }

Văn bản và bản địa hóa

Tab có văn bản, ngoài vai trò và liên kết đến đối tượng, còn chứa các cột sau:


  • Mã: một khóa văn bản để bản địa hóa
  • Hộp kiểm bản địa hóa: một chỉ báo rằng trường văn bản có thể bản địa hóa
  • Giá trị: nội dung văn bản hiện tại của đối tượng
  • Bản địa hóa: văn bản hiện tại được tìm thấy bằng khóa từ trường Mã



Thư viện không bao gồm một hệ thống con tích hợp để làm việc với các bản dịch. Để kết nối hệ thống bản địa hóa của bạn, bạn cần tạo triển khai giao diện ILocalizationProvider . Điều này có thể được xây dựng, ví dụ, dựa trên Back-end, ScriptableObjects hoặc Google Sheets của bạn.


 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; } }


Bằng cách nhấp vào nút Sao chép Bản địa hóa, nội dung của các cột Mã và Giá trị sẽ được sao chép vào khay nhớ tạm ở định dạng phù hợp để dán vào Google Trang tính.

Bên trong thanh phân

Thông thường, để tạo điều kiện tái sử dụng, các phần riêng biệt của giao diện người dùng được trích xuất thành các cấu trúc tiền chế riêng biệt. StatefulComponent cũng cho phép chúng ta tạo một hệ thống phân cấp các thành phần, trong đó mỗi thành phần chỉ hoạt động với các thành phần giao diện con của chính nó.


Trên tab Inner Comps, bạn có thể gán vai trò cho các thành phần bên trong:



Vai trò được định cấu hình có thể được sử dụng trong mã tương tự như các loại phần tử khác:


 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");

Hộp đựng

Để tạo danh sách các phần tử tương tự, bạn có thể sử dụng thành phần ContainerView . Bạn cần chỉ định prefab để khởi tạo và đối tượng gốc (tùy chọn). Trong chế độ Chỉnh sửa, bạn có thể thêm và xóa các phần tử bằng StatefulComponent :



Thật thuận tiện khi sử dụng StatefulComponent để đánh dấu nội dung của các prefab được khởi tạo. Trong Thời gian chạy, bạn có thể sử dụng các phương thức AddInstance<T> , AddStatefulComponent hoặc FillWithItems để điền vào vùng chứa:


 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); });


Nếu tiêu chuẩn Object.Instantiate() không phù hợp với bạn để tạo đối tượng, bạn có thể ghi đè hành vi này, ví dụ: để khởi tạo bằng Zenject:


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


Các Thành phần và Bộ chứa bên trong cung cấp khả năng lồng nhau tĩnh và động cho StatefulComponent tương ứng.


Chúng tôi đã xem xét việc đánh dấu prefabs, bản địa hóa và khởi tạo. Bây giờ là lúc chuyển sang phần thú vị nhất — phát triển giao diện người dùng dựa trên trạng thái.

Những trạng thái

Chúng tôi sẽ coi khái niệm trạng thái là một tập hợp các thay đổi được đặt tên đối với nhà lắp ghép. Tên trong trường hợp này là một vai trò từ StateRole enum và các ví dụ về các thay đổi đối với prefab có thể là:


  • Bật và tắt GameObject
  • Thay thế các họa tiết hoặc vật liệu cho các đối tượng Hình ảnh
  • Di chuyển đối tượng trên màn hình
  • Thay đổi văn bản và sự xuất hiện của chúng
  • chơi hoạt hình
  • Và cứ thế — bạn có thể thêm các loại thao tác đối tượng của riêng mình


Một tập hợp các thay đổi (Mô tả trạng thái) có thể được định cấu hình trên tab Trạng thái. Trạng thái được định cấu hình có thể được áp dụng trực tiếp từ trình kiểm tra:



Trạng thái được định cấu hình có thể được áp dụng từ mã bằng phương thức SetState :


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


Trên tab Công cụ, khi tham số Áp dụng trạng thái ban đầu khi bật được bật, bạn có thể định cấu hình Trạng thái sẽ được áp dụng ngay lập tức khi khởi tạo đối tượng.


Việc sử dụng các trạng thái cho phép giảm đáng kể số lượng mã được yêu cầu ở cấp lớp Chế độ xem. Chỉ cần mô tả từng trạng thái trên màn hình của bạn dưới dạng một tập hợp các thay đổi trong StatefulComponent và áp dụng Trạng thái cần thiết từ mã tùy thuộc vào tình huống trò chơi.

cây nhà nước

Trên thực tế, việc phát triển giao diện người dùng dựa trên các trạng thái là vô cùng tiện lợi. Nhiều đến mức, theo thời gian, nó dẫn đến một vấn đề khác — khi dự án phát triển, danh sách các trạng thái cho một cửa sổ duy nhất có thể dài đến mức không kiểm soát được và do đó trở nên khó điều hướng. Ngoài ra, có những trạng thái chỉ có ý nghĩa trong bối cảnh của một số trạng thái khác. Để giải quyết vấn đề này, Statful UI có một công cụ khác: State Tree. Bạn có thể truy cập nó bằng cách nhấp vào nút State Tree Editor trong tab States.


Giả sử chúng ta cần tạo một cửa sổ phần thưởng cho một chiếc rương. Cửa sổ có 3 giai đoạn:


  • Hoạt hình giới thiệu rương (bang Intro )
  • Sự xuất hiện vòng lặp của ba loại phần thưởng khác nhau từ rương (trạng thái Tiền , Biểu tượng cảm xúcThẻ , tùy thuộc vào trạng thái Phần thưởng , sẽ kích hoạt hoạt ảnh của phần thưởng xuất hiện từ rương)
  • Hiển thị tất cả các phần thưởng được trao trong một danh sách (nêu Kết quả )


Các trạng thái gốc (trong ví dụ này là Reward ) được áp dụng mỗi khi các trạng thái con được gọi:



Việc quản lý một StatefulComponent được định cấu hình chỉ cần một lượng mã đơn giản và dễ hiểu tối thiểu để điền vào các thành phần dữ liệu cần thiết và chuyển đổi trạng thái vào đúng thời điểm:


 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)); }

Tài liệu & API có trạng thái

Các vai trò nhằm cung cấp một cách thuận tiện và rõ ràng để đặt tên cho các liên kết và trạng thái để sử dụng sau này trong mã. Tuy nhiên, có những tình huống khi mô tả một trạng thái sẽ yêu cầu một cái tên quá dài và sẽ thuận tiện hơn nếu bạn để lại một nhận xét nhỏ về liên kết này trỏ đến điều gì hoặc trạng thái đó phản ánh hành vi nào. Đối với những trường hợp như vậy, mỗi liên kết và trạng thái trong StatefulComponent cho phép bạn thêm mô tả:



Bạn có thể đã nhận thấy các nút Sao chép API và Sao chép Tài liệu trên mỗi tab — những thông tin sao chép này cho phần đã chọn. Ngoài những nút đó, còn có các nút tương tự trong tab Công cụ — những nút này sao chép thông tin cho tất cả các phần cùng một lúc. Khi bạn nhấp vào nút Sao chép API, mã được tạo để quản lý đối tượng StatfulComponent này sẽ được sao chép vào khay nhớ tạm. Đây là một ví dụ cho cửa sổ phần thưởng của chúng tôi:


 // 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);


Khi bạn nhấp vào nút Sao chép tài liệu, tài liệu cho nhà lắp ghép này sẽ được sao chép vào khay nhớ tạm ở định dạng Markdown:


 ### 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


Rõ ràng việc mắc lỗi khi thực hiện màn hình này với hướng dẫn chi tiết như vậy là khá khó khăn. Bạn có thể dễ dàng duy trì thông tin cập nhật về tổ chức giao diện người dùng của mình trong cơ sở tri thức của dự án.


Đồng thời, Stateful UI cho phép ủy thác việc tạo các prefab UI. Trên thực tế, đánh dấu dựa trên trạng thái cho phép kiểm tra đầy đủ hành vi của nhà lắp ghép trước khi chuyển nó cho người lập trình. Điều này có nghĩa là các nhà thiết kế trò chơi , nhà thiết kế kỹ thuật hoặc thậm chí các nhà phát triển giao diện người dùng riêng biệt có thể chuẩn bị các bản dựng sẵn. Hơn nữa, vì API được tạo giữa mã và nhà lắp ghép, nên việc lập trình và định cấu hình nhà lắp ghép có thể được thực hiện song song! Tất cả những gì cần thiết là xây dựng API trước. Tuy nhiên, ngay cả khi nhiệm vụ định cấu hình prefabs vẫn thuộc về các lập trình viên, việc sử dụng Stateful UI sẽ tăng tốc đáng kể công việc này.

Phần kết luận

Như chúng ta đã thấy, Stateful UI đơn giản hóa đáng kể việc làm việc với các trạng thái phần tử UI. Các chu kỳ dài không còn cần thiết để tạo SerializeFields, biên dịch lại mã và tìm kiếm các tham chiếu trong số lượng lớn các trường lớp Chế độ xem. Trong chính các lớp View, không còn cần thiết phải viết một lượng lớn mã cho các hoạt động lặp đi lặp lại như bật và tắt đối tượng hoặc thay đổi màu văn bản.


Thư viện cho phép cách tiếp cận nhất quán để tổ chức bố cục trong một dự án, đánh dấu các đối tượng trong prefabs, tạo trạng thái, liên kết chúng với các thành phần giao diện người dùng và cung cấp API và tài liệu để quản lý giao diện người dùng. Nó cũng cho phép ủy thác việc tạo các prefab giao diện người dùng và tăng tốc độ làm việc với chúng.


Trong tương lai, lộ trình dự án bao gồm các mục sau:


  • Mở rộng khả năng của Trạng thái, hỗ trợ các loại thay đổi giao diện người dùng mới trong Mô tả, chẳng hạn như các loại hoạt ảnh mới, phát âm thanh ở trạng thái, v.v.


  • Thêm hỗ trợ cho bảng màu để tô màu văn bản và hình ảnh


  • Thêm hỗ trợ cho danh sách các mục có sử dụng lại GameObjects


  • Hỗ trợ nhiều phần tử Unity UI hơn


  • Tự động dỡ các văn bản đã thêm để bản địa hóa


  • Triển khai Khung kiểm tra. Vì chúng tôi đã đánh dấu đầy đủ các prefabs của mình nên chúng tôi có thể tạo các kịch bản dựa trên ScriptableObject dễ cài đặt theo định dạng sau:


    1. Nhấp vào nút ButtonRole.Settings

    2. Kiểm tra văn bản trong TextRole.SomeText bằng "một số giá trị"

    3. Kiểm tra hình ảnh trong ImageRole.SomeImage để đảm bảo nó bằng với một sprite nhất định


  • Một hệ thống hướng dẫn. Tương tự như thử nghiệm, bố cục được đánh dấu cho phép tạo các kịch bản hướng dẫn dựa trên ScriptableObject dưới dạng hướng dẫn như "Hiển thị con trỏ trên nút ButtonRole.UpgradeHero ".


Mã nguồn dự án có sẵn trên GitHub . Bạn có thể tạo các vấn đề hoặc đóng góp cho thư viện!