UI Toolkit进阶 - Template模板

上篇文章我们介绍了UI Toolkit,但是没有深入它的用法。本文就以一个项目界面从UGUI到UI Toolkit的改造过程为例,来学习一下较高阶的使用方法。

首先介绍一下本次的项目MarkovCraft,这个项目是在MarkovJunior基础上的一个二次开发,把原项目放在了Unity中,让用户在三维环境中看到动态的生成过程,并且加了一些导出的功能。那本次要改造的界面就是这个项目里面的模型预览面板,用于展示xml模型里面定义的生成流程。每个流程序列由一系列的节点(Node)或者说元素(Element)组成,其中像序列节点等特殊节点还可以包含子节点,形成嵌套结构。现有的UGUI实现能够实现功能,但是在处理较长模型的时候会生成大量UI GameObject引起严重的性能问题,所以本文考虑使用UI Toolkit对它重新进行实现。

从结构来讲,模型xml文档的结构与uxml界面文档的结构是很类似的,几乎可以一一对应过来,这就为我们的设计实现提供了很大的方便。从这个意义上讲,拿UI Toolkit实现要比之前拿UGUI来实现更合适,我们只需要将每种xml元素转换为对应的uxml元素即可。

为了方便讨论,我在这里贴一个简单的模型示例,右图是现有的UGUI实现显示出来的效果:

<!-- BacktrackerCycle.xml -->

<sequence values="BRGWA" origin="True">
  <union symbol="?" values="WA"/>
  <markov>
    <one in="RBB" out="GGR"/>
    <one in="RGG" out="WAR"/>
  </markov>
  <one in="R" out="W"/>
  <one in="WBW" out="WAW" steps="1"/>
  <all in="BBB/B?B" out="***/*B*"/>
  <all in="A" out="W"/>
</sequence>

我们先把模型中允许出现的所有类型的节点汇总一下,左侧是所有类型的节点对应C#类的继承树,右边括号里是节点在xml中的元素名:

Markov Junior Nodes Hierarchy
=============================

Node
|
|--Branch
|  |--MapNode               (map)
|  |--SequenceNode          (sequence)
|  |--MarkovNode            (markov)
|  |--WFCNode               (wfc)
|     |--OverlapNode           (+sample)
|     |--TileNode              (+tileset)
|
|--RuleNode
|  |--AllNode               (all)
|  |--OneNode               (one)
|  |--ParallelNode          (prl)
|
|--PathNode                 (path)
|--ConvChainNode            (overlap)
|--ConvolutionNode          (convolution)

=============================

其中,继承自Branch类的节点(mapseqeunce等),是能够包含子节点的,而其他的节点不能。继承自Rule类的几种节点(alloneprl)类似,都是替换节点,属性基本相同,后续可以共用一个UI模板,而不用每个各用各的。

我们分别把每种节点的界面做出来,界面的外观与原来的UGUI保持一致。以Rule Node为例,界面包含了节点名称的Label和为后续填充节点规则预留的槽位。这里可以临时加一个占位符元素看看节点的尺寸能不能正确地自适应,如图:

005_02.png

其中要指定元素为文档中的槽位(插入子元素的位置),只需要给该元素加一个 content-container属性即可,属性值任意填什么都可以。每种节点做完之后,可以手搓一个假的示例看一下带有嵌套结构整体的效果,验证一下各个节点的布局自适应有没有问题:

005_03.png

到这个地方,我们的文档资源就准备好了,接下来开始改代码。我在重制这部分代码的时候,并没有直接把原来的那套代码改掉,而是另外做了一套新的代码,和之前的实现平行存在,互不干涉。这样做有几个好处,一个是新版和旧版可以直接放在一起跑,可以很方便地进行比较,再一个就是写新的这套代码的时候可以很方便地去参考旧的代码,顺便找找之前的代码有没有什么小bug。这两套代码在逻辑上面基本一致,改起来工作量不大,这里说几个比较大的区别:

因为UI Toolkit中的界面元素不再是GameObject,所以我们对每个界面元素附加的C#对象也不再是组件的形式,故Node类就不再继承自MonoBehaviour类了。不过在之前的实现中,这些Node对象就是放在列表里面统一进行更新的,所以更新逻辑这一块完全不用修改,只需把创建和获取节点对象的代码进行相应修改即可。创建节点对象原来的做法是在节点的Prefab里面预先挂好对应的节点脚本,实例化Prefab之后使用GetComponent来获取节点脚本组件。而本次的做法是先拿到界面元素,然后把界面元素作为参数传给节点对象的构造函数,并且把这个引用保存在节点元素里面,类似下面这样。这种方式更符合面向对象编程的一般写法,而且无需像原来那样获取脚本组件,效率更高一些。

public class BaseGraphNodeV2
{
  // ...

  protected readonly VisualElement? m_NodeElement;

  public BaseGraphNodeV2(VisualElement nodeElement)
  {
      m_NodeElement = nodeElement;
  }

  // ...
}

更多的细节可以参考本次更新的commit,这里不再赘述。把代码修改完之后,就可以进行整体测试了。最终实现的视觉效果跟原来还是有稍许差异,但是不影响使用,所以也就不再过多纠结了。最后附一张改造前和改造后的合影:

005_04.png

posted @ 2024-04-09 17:27  DevBobcorn  阅读(37)  评论(0编辑  收藏  举报