Unity UI网格自定义应用

Unity UI网格自定义应用

引言

Unity UI是Unity官方支持的UI框架,在设计上有一定的灵活性和扩展性。UGUI除了RectTransform, Canvas,CanvasRender稍微底层的组件,主要组件如Image,Text等都是开源的。

UGUI基础结构

UGUI里面所有可以绘制与响应事件的图形组件都继承于Graphic。由于基础组件都支持遮罩层,实际上Image,Text这些并不直接继承Graphic,而是继承MaskableGraphic。在UGUI内部进行Graphic网格生成的时候,会去调用OnPopulateMesh方法。也就是说,当子类需要自定义网格时,只需要重载OnPopulateMesh这个方法就可以了。下面是该方法的声明:

public abstract class Graphic
   [Obsolete("Use OnPopulateMesh(VertexHelper vh) instead.", false)]
    protected virtual void OnPopulateMesh(Mesh m){
   }
    
 protected virtual void OnPopulateMesh(VertexHelper vh)
   {
   }
}

除了Graphic子类可以自定义网格外,还可以通过IMeshModifier接口来实现。比如一些效果组件Outline, Shadow等,就是通过这种方式实现的。下面是IMeshModifier接口的声明:

public interface IMeshModifier
{
   [Obsolete("use IMeshModifier.ModifyMesh (VertexHelper verts) instead", false)]
    void ModifyMesh(Mesh mesh);
    void ModifyMesh(VertexHelper verts);
}

可以看出ModifyMesh和OnPopulateMesh非常类似,如出一辙。事实上,UGUI在执行完OnPopulateMesh之后,会检测同一个GameObject上有没有实现了IMeshModifier接口的组件,有的话就会调用ModifyMesh方法的实现。值得注意的是,组件的挂载是有顺序的,也就是当同时有多个IMeshModifier组件时,会按照挂载顺序依次执行ModifyMesh。

事件UI层的处理

在某些情景下,我们需要一个UI层去接受事件,又不需要显示这一层UI。针对这种情况,一种简单的做法是挂载一个空Image,将Alpha设为0。但是这种做法有个缺陷,会造成无用的重复绘制,在一些手机平台上,会产生无用的额外的开销。最好的办法是自定义一个Graphic组件,但又不进行渲染。这可以通过自定义网格来实现,也就是清除网格。

在Quizdom项目里面,UIBlank就实现了这么一个机制:

[AddComponentMenu("UGUI/UIBlank", 1)]
public class UIBlank : MaskableGraphic
{
    protected UIBlank()
   {
        useLegacyMeshGeneration = false;
   }
 
    protected override void OnPopulateMesh(VertexHelper toFill)
   {
        toFill.Clear();
   }
}

值得注意的是,在构造函数里面申明了useLegacyMeshGeneration为false,也就是说不用Obsolete生成方法,只需要覆写OnPopulateMesh就可以了。方法里面很简单,该行代码就是把网格顶点信息清除。

圆形Mask优化

在处理人物头像的时候,往往需要头像显示成圆形的,而头像本身的图片是方的。在UGUI中,可以采用Mask组件实现这个功能,具体是给组件一个圆形白底不透明的遮罩图片。不幸的是,这种方式有比较大的缺陷。Mask组件是采用模板缓存去实现的,意味着Mask下面的UI跟Mask之外的UI不能进行Batch操作。这样割裂了原本一体的UI界面,导致了DrawCall的增加。由于头像可能会比较多,Mask的开销会很明显。如下图所示,一个Mask头像就占用了3个DrawCall(也就是Batch数)。

如果采用圆形网格的形式,就可以避免这种问题,只是会增加网格的内存占用。相比较而言,少量的内存换取DrawCall数的下降,还是值得的。

CircleImage脚本通过继承Image,覆写OnPopulateMesh的方式来实现圆的绘制。下面是简化的代码,基本思想是将圆等分成segements个扇形,以三角形近似扇形来实现的。等分得越多,越接近于圆。

protected override void OnPopulateMesh(VertexHelper vh)
{
    vh.Clear();
    float degreeDelta = 2 * Mathf.PI / segements;
    float tw = rectTransform.rect.width;
    float th = rectTransform.rect.height;
    float outerRadius = rectTransform.pivot.x * tw;
 
    Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;
    float uvCenterX = (uv.x + uv.z) * 0.5f;
    float uvCenterY = (uv.y + uv.w) * 0.5f;
    float uvScaleX = (uv.z - uv.x) / tw;
    float uvScaleY = (uv.w - uv.y) / th;
 
    var verticeCount = segements + 1;
    var curVertice = Vector2.zero;
    float curDegree = 0;
 
    var uiVertex = new UIVertex();
    uiVertex.color = color;
    uiVertex.position = curVertice;
    uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
   vh.AddVert(uiVertex);
 
    for (int i = 1; i < verticeCount; i++)
   {
        float cosA = Mathf.Cos(curDegree);
        float sinA = Mathf.Sin(curDegree);
        curVertice = new Vector2(cosA * outerRadius, sinA * outerRadius);
        curDegree += degreeDelta;
 
        uiVertex = new UIVertex();
        uiVertex.color = color;
        uiVertex.position = curVertice;
        uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
        vh.AddVert(uiVertex);
   }
 
    var triangleCount = segements * 3;
    for (int i = 0, vIdx = 1; i < triangleCount - 3; i += 3, vIdx++)
   {
        vh.AddTriangle(vIdx, 0, vIdx + 1);
   }
 
    vh.AddTriangle(verticeCount - 1, 0, 1);
}

采用CircleImage的DrawCall如下所示,只占用本身的绘制开销,DrawCall为1.

图文混排的处理

UGUI官方Text并不支持图文混排。一种图文混排的方式是将文字和图片合在一张图集上,但Text一般是动态字体,这种方式不太适合;另一方面,也不利于UGUI的batch操作,会导致DrawCall增加。一种可行的思路是,通过修改Text网格,给图片预留空间,然后将图片补上空缺的位置。

具体实现,看下面代码。值得注意的地方是,SetVerticesDirty方法。当该方法被调用时,说明网格需要重新生成,下一步需要执行OnPopulateMesh方法。

public override void SetVerticesDirty()
{
    base.SetVerticesDirty();
    UpdateQuadImage();
}

UpdateQuadImage的逻辑是,通过正则表达式去匹配原有文本中的<quad></quad>标签,然后用Image子物体去替换它们,具体代码可以参考附件工程。LinkImageText的实现,有一定局限性,它只支持单行文本。

protected override void OnPopulateMesh(VertexHelper toFill)
{
    var orignText = m_Text;
    m_Text = GetFinalText(orignText);
    base.OnPopulateMesh(toFill);
    m_Text = orignText;
 
    float offsetX = 0;
 
    UIVertex vert = new UIVertex();
    for (var i = 0; i < m_ImagesVertexIndex.Count; i++)
   {
        var endIndex = m_ImagesVertexIndex[i];
        var rt = m_ImagesPool[i].rectTransform;
        var size = rt.sizeDelta;
        if (endIndex < toFill.currentVertCount)
       {
            toFill.PopulateUIVertex(ref vert, endIndex);
            rt.localPosition = new Vector2(vert.position.x + size.x / 2, vert.position.y + size.y / 2 + offsetY);
            offsetX = offsetX + size.x;
 
            for (int j = endIndex - 3; j < toFill.currentVertCount; j++)
           {
                toFill.PopulateUIVertex(ref vert, j);
                vert.position += new Vector3(size.x, 0, 0);
                toFill.SetUIVertex(vert, j);
           }
       }
   }
 
    var offset = offsetX;
    if (alignment == TextAnchor.LowerLeft || alignment == TextAnchor.MiddleLeft ||
        alignment == TextAnchor.UpperLeft)
   {
        offset = 0f;
   }
    else if (alignment == TextAnchor.LowerCenter || alignment == TextAnchor.MiddleCenter ||
             alignment == TextAnchor.UpperCenter)
   {
        offset = offsetX * -1f / 2;
   }
    else if (alignment == TextAnchor.LowerRight || alignment == TextAnchor.MiddleRight ||
             alignment == TextAnchor.UpperRight)
   {
        offset = offsetX * -1f;
   }
 
    for (int i = 0; i < toFill.currentVertCount; i++)
   {
        toFill.PopulateUIVertex(ref vert, i);
        vert.position += new Vector3(offset, 0, 0);
        toFill.SetUIVertex(vert, i);
   }
 
    for (var i = 0; i < m_ImagesVertexIndex.Count; i++)
   {
        var rt = m_ImagesPool[i].rectTransform;
        rt.localPosition += new Vector3(offset, 0f, 0f);
   }
}

在这个实现中,利用了父类Text的网格生成结果,在这个基础上进行文本和图片位置的调整。这个主要由于Unity Text生成规则并不开放,只提供TextGenerator类可以调用。Text内部会使用构造好的TextGenerator进行生成。这里就不再展开。参考工程效果图如下:

总结

Unity UI框架的可扩展性相对NGUI来说要好一点,合理利用扩展接口,可以不局限于现有的UI组件,同时也可以提高UI的渲染效率。本文针对网格自定义这一点,列举了一些应用场景和优化的点,主要供大家扩展思路。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值