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ệ (uGUI), công nghệ này vẫn được khuyến nghị cho Thời gian chạy . Cách tiếp cận được mô tả không áp dụng cho , hoặc các hệ thống xây dựng UI khác. Giao diện người dùng thống nhất theo tài liệu UI Toolkit IMGUI Thông thường nhất trong các dự án , 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ừ và được thêm vào một số lượng lớn các trường . 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). Unity MonoBehaviour SerializeField 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 , 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. SerializeField Kết quả là, chúng tôi kết thúc với các lớp con 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). MonoBehaviour 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: Đầu tiên, bất kỳ thay đổi nào trong trạng thái cửa sổ đều xảy ra . Để 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 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). khi sử dụng mã SerializeField Một cách tiếp cận khác có thể được mô tả là “ ”. 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. Animator toàn năng 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 . 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: StatefulComponent Mỗi liên kết được đặt tên dựa trên của nó. Từ góc độ mã, tập hợp các vai trò là một 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.): vai trò enum 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 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ử này có thể được sử dụng ngay sau khi tạo. enum enum Để đơ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ộ 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 và biết vai trò của hình ảnh mong muốn để thay thế sprite của nó chẳng hạn. SerializeField StatefulComponent 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 và UnityEngine.UI.Text TextMeshProUGUI TextInputs, bao gồm và UnityEngine.UI.InputField TMP_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 hoặc kế thừa lớp từ : StatefulComponent 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 . Đ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. ILocalizationProvider 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. 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ó. StatefulComponent 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 . 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 : ContainerView StatefulComponent Thật thuận tiện khi sử dụng để đá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 , hoặc để điền vào vùng chứa: StatefulComponent AddInstance<T> AddStatefulComponent FillWithItems 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 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: Object.Instantiate() 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 tương ứng. StatefulComponent 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ừ enum và các ví dụ về các thay đổi đối với prefab có thể là: StateRole 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ố đượ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. Áp dụng trạng thái ban đầu khi bật 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 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. StatefulComponent 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 , và , tùy thuộc vào trạng thái , sẽ kích hoạt hoạt ảnh của phần thưởng xuất hiện từ rương) Tiền Biểu tượng cảm xúc Thẻ Phần thưở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 ) được áp dụng mỗi khi các trạng thái con được gọi: là Reward Việc quản lý một đượ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: StatefulComponent 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 cho phép bạn thêm mô tả: StatefulComponent 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 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: StatfulComponent // 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à , 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. các nhà thiết kế trò chơi 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: Nhấp vào nút ButtonRole.Settings Kiểm tra văn bản trong bằng "một số giá trị" TextRole.SomeText Kiểm tra hình ảnh trong để đảm bảo nó bằng với một sprite nhất định ImageRole.SomeImage 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 . Bạn có thể tạo các vấn đề hoặc đóng góp cho thư viện! Mã nguồn dự án có sẵn trên GitHub