Unity:嘗試使用Zenject、Unity的Localization在地化插件與UIToolkit在一起
前陣子接觸到Unity的新的UI工具UIToolkit,讓我想到以前碰到網頁排版和樣式要寫CSS的日子。很好奇目前重製版專案裡的UI是否也能順利轉換到UIToolkit上,由於我UI在功能上有用到Zenject所以想趁著這陣子有空來測試看看,能不能順利轉換。
之所以寫這篇文章是因為網路上查到有用 UIToolkit和Zenject在一起的人並不多,所以想拋磚引玉看看有沒有人也有類似經驗可分享。
測試對象:一個會使用到Zenject和Localization功能的按鈕
測試的項目很簡單,挑一個比較有代表性的元素做測試。
我選擇的是一個帶有文字敘述和圖片的按鈕,這個按鈕其實是展示畫作圖片的按鈕。UGUI版本中是一個Prefab,上面有個元件可以指定這個按鈕的圖片和文字敘述,文字敘述會由Localization這個插件負責載入在地化版本的文本,而這個按鈕的功能很簡單,按下去後會使用Zenject的Signal發出訊息,會滿版顯示這個按鈕的圖片和文字敘述,以下的影片為UGUI版本的展示:
由於我選擇的按鈕文字敘述和圖片是綁在一起的, 所以我就用ScriptableObject包起來,裡面裝有LocalizedString和Sprite。其中有個事件,用來通知在地化文本的語言變更,UI的部分接收到通知後再取得最新的在地化文本。
怎麼寫客製化的VisualElement
我這邊客製化的VisualElement稱作ImageBoxElement,以下的程式也會用這個名字示範。
客製化的VisualElement裡面的內容可以透過程式添加,類似這樣:
ImageBoxElement.Add(new Label("NewLabel"));
或者是透過UIBuilder做出一個範本(類型是VisualTreeAsset),並透過在程式中載入的方式添加,有點像是在UGUI排好UI元素然後做成Prefab的概念。記住裡面會用程式控制的元素記得命名。
如下圖就是我製作的範本,稱作ImageBox:
載入並動態生成的方法,我是用Addressables載入:
public class ImageBoxElement : VisualElement { //_paintData是儲存畫作資料的ScriptableObject,怎麼給就看個人做法 private PaintingData _paintData; private Label _header = default; private VisualElement _image = default; private Label _name = default; } public ImageBoxElement() { //從Addressables載入製作好的UI範本 VisualTreeAsset tree = Addressables.LoadAsset<VisualTreeAsset>("ImageBox").WaitForCompletion(); //從範本裡面複製一份副本到客製化的VisualElement裡 tree.CloneTree(this); //把底下有關文本和圖片的VisualElement存起來,提供之後修改內容使用 _header = this.Q<Label>("Header"); _image = this.Q<VisualElement>("Image"); _name = this.Q<Label>("Name"); } }
如此一來客製化的VisualElement裡面就會有在UIBuilder做的ImageBox裡所含的UI。而使用上跟一般內建的VisualElement差不多,可以用程式添加或是在UIBuilder中拖曳添加。
客製化的VisualElement中處理文字在地化
LocalizedString.StringChanged += (str) => VisualElement.Q<Label>("Header").text = str;
由於我的在地化文本有使用到SmartString,所以我並沒有直接用這種方法,LocalizedString.StringChanged只是用來通知文本變更,然後透過LocalizedString.GetLocalizedString取得處理好的文本,這些部分被我封裝在ScriptableObject裡。
有關處理文字在地化的部分差不多了,再來就是怎麼讓這些客製化VisualElement使用Zenject的Signal互相溝通呢?
客製化的VisualElement與Zenject Signal
[Inject] readonly SignalBus _signalBus;
並且在Installer裡將ImageBoxElement抓出來,並做Binding,才能完成BindSignal和注入SignalBus。
為了將我們做好的UI顯示在畫面上和抓取ImageBoxElement,就需要在場上放一個GameObject並掛上UIDocument元件,將我們做好的UIVisualTreeAsset放上去,這樣就能在畫面上看到UI,並且可以透過程式控制UI的元素,並抓取底下的VisualElement。
從UIDocument取得最上層的rootVisualElement後查找所有ImageBoxElement,先儲存起來,並用匿名方法Binding。然後針對每一個ImageBoxElement做注入,否則會Zenject不會注入,因為ImageBoxElement生成不是由Zenject處理,而且生成的時間很早。
更新:Zenject 9.3.1版已經有內建處理了,無須手動操作。
有關注入,手動對每個物件要求注入是我目前是出來唯一可以做到的方法,不知道有沒有其他更好的方法可以做到。🤔
IEnumerable<ImageBoxElement> elements = UIDocument.rootVisualElement.Query<ImageBoxElement>().ToList().OfType<ImageBoxElement>(); Container.Bind<ImageBoxElement>().FromMethodMultiple((ctx) => elements); foreach (ImageBoxElement element in elements) { Container.QueueForInject(element); }
完成注入後,剩下的就相對簡單,只需要處理訊息發送和接收。與UGUI時使用Zenject的一樣,首先是在執行完CloneTree後註冊Callback以在滑鼠按下時發送Signal,Signal裡面會裝有存放畫作資料的ScriptableObject。
//傳送畫作資料的Signal public class TestEvent { public PaintingData Data; } //在ImageBoxElement建構式中CloneTree之後 this.Q<Button>("Box").RegisterCallback<ClickEvent>((click) => _signalBus.Fire(new TestEvent() { Data = _paintData }));然後處理接收訊息的部分
//在Installer中 Container.DeclareSignal<TestEvent>(); Container.BindSignal<TestEvent>().ToMethod<ImageBoxElement>(x => x.ReactWhenOnClick).FromResolveAll(); //在ImageBoxElement中,這邊只是簡單示範開關VisualElement和更換內容而已 public void ReactWhenOnClick(TestEvent t) { //只有名稱叫做Target的ImageBoxElement才需要反應 if (this.name == "Target") { //開關顯示VisualElement if (this.style.display == DisplayStyle.Flex) this.style.display = DisplayStyle.None; else this.style.display = DisplayStyle.Flex; _header.text = t.Data.GetHeader(); _image.style.backgroundImage = new StyleBackground(t.Data.GetImage()); _name.text = t.Data.GetSubtitile(); } }這邊寫完後大致上功能就完成了,剩下的功能就差不多完成了,進入PlayMode測試就是最開頭影片的樣子。
留言
張貼留言
₍₍ς(OωO ς)⁾⁾