|
My Git:https://github.com/S0nrEir
My Blog:主页 - S0nrEir's Blog
第三章-数据表与程序
3.1数据表的种类
- 代码:直接写死,通常只存在于Demo阶段或mini game。制作快,改动麻烦,有迭代问题。
- 文本数据:json,xml,csv等格式。直观,易修改。导表工具。
- 比特流:.bytes,空间小,解析快,通用性差,不直观。适合release或正式版本。
3.2制作方式
自动化工具,Jekins,导表工具比如LuBan
3.3多语言
常量表,Key / Value,或者多语言对应多个语言表
<hr/>
第四章-UI
UGUI,NGUI,FGUI等
4.2 UGUI源码
3D网格下建立的系统,换言之,UI也有网格。
DrawCall问题:图集
问题在于,UGUI并非将所有的网格和材质合并成一个网格,这会导致前后层级问题,它只合并相同层级的元素,如果有任何移动或销毁UGUI就会销毁这个网格,然后构建一个新的(重绘)。如果时刻在移动这些元素,UGUI就会不停的拆分合并,导致性能问题。
4.4 事件系统
UGUI源码地址
事件系统结构如下:
- EventSystem
- EventData
- AxisEventData.cs
- BaseEventData.cs
- PointerEventData.cs
- InputModules
- BaseInputModule.cs
- PointerInputModule.cs
- StandaloneInputModule.cs
- TouchInputModule.cs
- Raycasters.cs
- BaseRaycasters.cs
- Physics2DRaycasters.cs
- PhysicsRaycasters.cs
先来看事件数据模块
它描述了事件的位置,对应的物体、类型等,比如PointerEventData
//继承自BaseEventData,它的额外逻辑不多,只需要记录滚轮信息
public class AxisEventData : BaseEventData
{
public Vector2 moveVector { get; set; }
public MoveDirection moveDir { get; set; }
public AxisEventData(EventSystem eventSystem)
: base(eventSystem)
{
moveVector = Vector2.zero;
moveDir = MoveDirection.None;
}
}
``
/// The object that received &#39;OnPointerEnter&#39;.
public GameObject pointerEnter { get; set; }
// The object that received OnPointerDown
private GameObject m_PointerPress;
/// The raw GameObject for the last press event. This means that it is the &#39;pressed&#39; GameObject even if it can not receive the press event itself.
public GameObject lastPress { get; private set; }
/// The object that the press happened on even if it can not handle the press event.
public GameObject rawPointerPress { get; set; }
/// The object that is receiving &#39;OnDrag&#39;.
public GameObject pointerDrag { get; set; }
/// RaycastResult associated with the current event.
public RaycastResult pointerCurrentRaycast { get; set; }
/// RaycastResult associated with the pointer press.
public RaycastResult pointerPressRaycast { get; set; }
public List<GameObject> hovered = new List<GameObject>();
/// Is it possible to click this frame
public bool eligibleForClick { get; set; }
/// Id of the pointer (touch id).
public int pointerId { get; set; }
/// Current pointer position.
public Vector2 position { get; set; }
/// Pointer delta since last update.
public Vector2 delta { get; set; }
/// Position of the press.
public Vector2 pressPosition { get; set; }
/// World-space position where a ray cast into the screen hits something
[Obsolete(&#34;Use either pointerCurrentRaycast.worldPosition or pointerPressRaycast.worldPosition&#34;)]
public Vector3 worldPosition { get; set; }
/// World-space normal where a ray cast into the screen hits something
[Obsolete(&#34;Use either pointerCurrentRaycast.worldNormal or pointerPressRaycast.worldNormal&#34;)]
public Vector3 worldNormal { get; set; }
/// The last time a click event was sent. Used for double click
public float clickTime { get; set; }``
PointerEventData存储了大部分事件系统逻辑需要的数据。
事件数据模块为事件逻辑提供数据。
输入事件捕获
由BaseInputModule,PointerInputModule,StandaloneInputModule,TouchInputModule四个类组成。BaseInputModule是基类。
他们的结构如下
BaseInputModule
|
PointerInputModule(关于点位的输入逻辑,输入的类型和状态)
/ \
StadaloneInpoutModule TouchInputModule
(标准键鼠) (触摸输入)
/// <summary>
/// 处理所有的鼠标事件
/// </summary>
protected void ProcessMouseEvent(int id)
{
var mouseData = GetMousePointerEventData(id);
var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;
// Process the first mouse button fully
// 处理鼠标左键相关的事件
ProcessMousePress(leftButtonData);
ProcessMove(leftButtonData.buttonData);
ProcessDrag(leftButtonData.buttonData);
// Now process right / middle clicks
// 处理鼠标右键和中建的点击事件
ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData);
ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData);
ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData);
ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData);
//滚轮事件处理
if (!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f))
{
var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);
ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);
}
}
这是StandaloneInputModule 的主函数 ProcessMouseEvent。处理了鼠标的各种逻辑。
/// <summary>
/// Process the current mouse press.
/// 处理鼠标按下事件
/// </summary>
protected void ProcessMousePress(MouseButtonEventData data)
{
var pointerEvent = data.buttonData;
var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;
// PointerDown notification
// 按下通知
if (data.PressedThisFrame())
{
pointerEvent.eligibleForClick = true;
pointerEvent.delta = Vector2.zero;
pointerEvent.dragging = false;
pointerEvent.useDragThreshold = true;
pointerEvent.pressPosition = pointerEvent.position;
pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;
DeselectIfSelectionChanged(currentOverGo, pointerEvent);
// 搜索元件中按下事件的句柄,并执行按下事件句柄
var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);
// didnt find a press handler... search for a click handler
// 搜索后找不到句柄,就设置一个自己的
if (newPressed == null)
newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
// Debug.Log(&#34;Pressed: &#34; + newPressed);
float time = Time.unscaledTime;
if (newPressed == pointerEvent.lastPress)
{
var diffTime = time - pointerEvent.clickTime;
if (diffTime < 0.3f)
++pointerEvent.clickCount;
else
pointerEvent.clickCount = 1;
pointerEvent.clickTime = time;
}
else
{
pointerEvent.clickCount = 1;
}
pointerEvent.pointerPress = newPressed;
pointerEvent.rawPointerPress = currentOverGo;
pointerEvent.clickTime = time;
// Save the drag handler as well
// 保存拖拽信息
pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);
// 执行拖拽启动事件句柄
if (pointerEvent.pointerDrag != null)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
}
// PointerUp notification
// 抬起通知
if (data.ReleasedThisFrame())
{
//执行抬起事件的句柄
// Debug.Log(&#34;Executing pressup on: &#34; + pointer.pointerPress);
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);
// Debug.Log(&#34;KeyCode: &#34; + pointer.eventData.keyCode);
var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
// 如果抬起时与按下时为同一个元素,那就是点击
if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
{
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
}
// 否则也可能是拖拽的释放
else if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
{
ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
}
pointerEvent.eligibleForClick = false;
pointerEvent.pointerPress = null;
pointerEvent.rawPointerPress = null;
// 如果正在拖拽则抬起事件等于拖拽结束事件
if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents<span class="p">.endDragHandler);
pointerEvent.dragging = false;
pointerEvent.pointerDrag = null;
// 如果当前接收事件的物体和事件的刚开始的物体不一致,则对两个物体做进和出的事件处理
if (currentOverGo != pointerEvent.pointerEnter)
{
HandlePointerExitAndEnter(pointerEvent, null);
HandlePointerExitAndEnter(pointerEvent, currentOverGo);
}
}
}
这是ProcessMousePress 处理鼠标按下事件的逻辑。
ProcessDrag函数:与ProcessMousePress类似
ProcessMove:刷帧调用处理函数HandlePointerExitAndEnter
射线检测
从相机的屏幕位置进行射线检测,将结果返给事件模块处理。三大块:2D射线,3D射线,图形射线GraphicRaycaster。2D射线检测以层级作为碰撞结果排序,3D以距离。GraphicRaycaster不依赖于射线碰撞,而是遍历所有可点击的UI元素来判断该响应哪个UI元素。
GraphicRaycaster部分代码:
[NonSerialized] static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, List<Graphic> results)
{
//遍历找到的所有Graphic实例
var foundGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
for (int i = 0; i < foundGraphics.Count; ++i)
{
Graphic graphic = foundGraphics;
// -1 means it hasn&#39;t been processed by the canvas, which means it isn&#39;t actually drawn
//-1表示它还没有被canvas处理,如果没有勾选raycastTarget也不处理
if (graphic.depth == -1 || !graphic.raycastTarget)
continue;
//不在该Graphic实例的RectTransform范围内。
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera))
continue;
//如果射线能打到,就把它放到检测结果排序列表里
if (graphic.Raycast(pointerPosition, eventCamera))
{
s_SortedGraphics.Add(graphic);
}
}
//根据深度排序
s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
//添加到结果集并返回
for (int i = 0; i < s_SortedGraphics.Count; ++i)
results.Add(s_SortedGraphics);
// Debug.Log (cast.ToString());
s_SortedGraphics.Clear();
}
事件处理
主要逻辑在EventSystem类(射线碰撞后引起的各类事件,是否成立,回调,刷帧等待),EventInterfaces、EventTrigger、EventTriggerType定义了事件回调,EventExecute包含了执行事件的回调接口。
EventSystem是唯一继承MonoBhvr并且刷帧检查的事件类,即所有UI事件的入口都在它的Update函数被触发,它调用输入,碰撞相关模块组成自己的逻辑。
4.4核心模块
几部分:Culling、Layout、MaterialModifiers、SpecializedCollection、Utility、VertexModifiers
Culling
对应模型裁剪的工具类,一般用在Mask上。两个比较重要的函数。
//找出多个RectMask2D的重叠部分,计算重叠区域
public statc Rect FindCullAndClipWorldRect(List<RectMask2D> rectMaskParents, out bool validRect)
{
if (rectMaskParents.Count == 0)
{
validRect = false;
return new Rect();
}
var compoundRect = rectMaskParents[0].canvasRect;
for (var i = 0; i < rectMaskParents.Count; ++i)
compoundRect = RectIntersect(compoundRect, rectMaskParents.canvasRect);
var cull = compoundRect.width <= 0 || compoundRect.height <= 0;
if (cull)
{
validRect = false;
return new Rect();
}
Vector3 point1 = new Vector3(compoundRect.x, compoundRect.y, 0.0f);
Vector3 point2 = new Vector3(compoundRect.x + compoundRect.width, compoundRect.y + compoundRect.height, 0.0f);
validRect = true;
return new Rect(point1.x, point1.y, point2.x - point1.x, point2.y - point1.y);
}
//计算两个矩阵的重叠部分
private static Rect RectIntersect(Rect a, Rect b)
{
float xMin = Mathf.Max(a.x, b.x);
float xMax = Mathf.Min(a.x + a.width, b.x + b.width);
float yMin = Mathf.Max(a.y, b.y);
float yMax = Mathf.Min(a.y + a.height, b.y + b.height);
if (xMax >= xMin && yMax >= yMin)
return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
return new Rect(0f, 0f, 0f, 0f);
}
Layout
主要负责布局(横、纵、Grid等)、自适应(CanvasScaler、ContantSizeFitter等)
//根据屏幕尺寸处理缩放
protected virtual void HandleScaleWithScreenSize()
{
Vector2 screenSize = new Vector2(Screen.width, Screen.height);
float scaleFactor = 0;
switch (m_ScreenMatchMode)
{
//不同的适配模式
case ScreenMatchMode.MatchWidthOrHeight:
{
// We take the log of the relative width and height before taking the average.
// Then we transform it back in the original space.
// the reason to transform in and out of logarithmic space is to have better behavior.
// If one axis has twice resolution and the other has half, it should even out if widthOrHeight value is at 0.5.
// In normal space the average would be (0.5 + 2) / 2 = 1.25
// In logarithmic space the average is (-1 + 1) / 2 = 0
float logWidth = Mathf.Log(screenSize.x / m_ReferenceResolution.x, kLogBase);
float logHeight = Mathf.Log(screenSize.y / m_ReferenceResolution.y, kLogBase);
float logWeightedAverage = Mathf.Lerp(logWidth, logHeight, m_MatchWidthOrHeight);
scaleFactor = Mathf.Pow(kLogBase, logWeightedAverage);
break;
}
case ScreenMatchMode.Expand:
{
scaleFactor = Mathf.Min(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);
break;
}
case ScreenMatchMode.Shrink:
{
scaleFactor = Mathf.Max(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);
break;
}
}
SetScaleFactor(scaleFactor);
SetReferencePixelsPerUnit(m_ReferencePixelsPerUnit);
}
这是CanvasScaler的核心函数之一。
IMaterialModifier
为Mask修改材质球准备。
IndexedSet
一个封装的容器类,提高了移除元素、元素包含判断的性能。
ListPool
对象池
VertexModifiers
比较重要,用于保存生成UI Mesh的数据。因为UI Mesh的变动频率高,为了做优化,它用到了IndexedSet和ListPool。但它不做计算,这部分由各自的Graphic实例完成。它只负责存储数据。
BaseMeshEffect:抽象基类。
IMeshModifier:关键接口,Graphic类会获取所有拥有这个接口的组件,遍历他们并且调用IMeshModifier.ModifyMesh来修改图像网格。
//这是Outline组件的一段代码,用于实现描边、阴影
protected void ApplyShadowZeroAlloc(List<UIVertex> verts, Color32 color, int start, int end, float x, float y)
{
UIVertex vt;
var neededCpacity = verts.Count * 2;
if (verts.Capacity </span> <span class="n">neededCpacity)
verts.Capacity = neededCpacity;
//在原有mesh的基础上加入新顶点,复制原顶点的数据修改颜色并外扩
for (int i = start; i < end; ++i)
{
vt = verts;
verts.Add(vt);
Vector3 v = vt.position;
v.x += x;
v.y += y;
vt.position = v;
var newColor = color;
if (m_UseGraphicAlpha)
newColor.a = (byte)((newColor.a * verts.color.a) / 255);
vt.color = newColor;
verts = vt;
}
}
Graphic
//通知元素重新布局,重新构建网格,重新构建材质
public virtual void SetAllDirty()
{
SetLayoutDirty();
SetVerticesDirty();
SetMaterialDirty();
}
public virtual void SetLayoutDirty()
{
if (!IsActive())
return;
//LayoutRebuilder.MarkLayoutForRebuild重新布局然后调用回调
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
if (m_OnDirtyLayoutCallback != null)
m_OnDirtyLayoutCallback();
}
public virtual void SetVerticesDirty()
{
if (!IsActive())
return;
m_VertsDirty = true;
//通知canvas重新构建该实例的网格
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
if (m_OnDirtyVertsCallback != null)
m_OnDirtyVertsCallback();
}
public virtual void SetMaterialDirty()
{
if (!IsActive())
return;
m_MaterialDirty = true;
//通知canvas重新构建该实例的材质
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
if (m_OnDirtyMaterialCallback != null)
m_OnDirtyMaterialCallback();
}
上面的代码可以看到重新构建时并不是立即构建,而是将自己通过RegisterCanvasElementForGraphicRebuild将自己加入到IndexedSet,等待下次重构。
//这是CanvasUpdateRegistry中的相关函数。InternalRegisterCanvasElementForGraphicRebuild 将元素放入重构队列中等待下一次重构。
//
//
//
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}
public static bool TryRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
return instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}
private bool <span class="n">InternalRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
//
if (m_PerformingGraphicUpdate)
{
Debug.LogError(string.Format(&#34;Trying to add {0} for graphic rebuild while we are already inside a graphic rebuild loop. This is not supported.&#34;, element));
return false;
}
//如果有就跳过
if (m_GraphicRebuildQueue.Contains(element))
return false;
//将实例添加到重建队列
m_GraphicRebuildQueue.Add(element);
return true;
}
private static readonly Comparison<ICanvasElement> s_SortLayoutFunction = SortLayoutList;
private void PerformUpdate()
{
CleanInvalidItems();
m_PerformingLayoutUpdate = true;
//布局重构,先进行排序
m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
{
for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
{
var rebuild = instance.m_LayoutRebuildQueue[j];
try
{
if (ObjectValidForUpdate(rebuild))
rebuild.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, rebuild.transform);
}
}
}
for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
m_LayoutRebuildQueue.LayoutComplete();
instance.m_LayoutRebuildQueue.Clear();
m_PerformingLayoutUpdate = false;
// 裁剪
// now layout is complete do culling...
ClipperRegistry.instance.Cull();
//元素重构
m_PerformingGraphicUpdate = true;
for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
{
for (var k = 0; k < instance.m_GraphicRebuildQueue.Count; k++)
{
try
{
//取出每个实例,做检查然后调用他们的Rebuild函数
var element = instance.m_GraphicRebuildQueue[k];
if (ObjectValidForUpdate(element))
element.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, instance.m_GraphicRebuildQueue[k].transform);
}
}
}
//Rebuild完成之后调用LayoutComplete
for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
m_GraphicRebuildQueue.LayoutComplete();
//清掉重构队列
instance.m_GraphicRebuildQueue.Clear();
m_PerformingGraphicUpdate = false;
}
先将需要重新布局的元素挨个Rebuild,布局后进行裁剪,裁剪后对每个需要重构的元素调用Rebuild。
另一个重要函数是Graphic的网格重建函数
private void DoMeshGeneration()
{
//合法就调用OnPopulateMesh函数,否则就清掉网格数据因为graphic不合法
if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
OnPopulateMesh(s_VertexHelper);
else
s_VertexHelper.Clear(); // clear the vertex helper so invalid graphics dont draw.
var components = ListPool<Component>.Get();
GetComponents(typeof(IMeshModifier), components);
for (var i = 0; i < components.Count; i++)
((IMeshModifier)components).ModifyMesh(s_VertexHelper);
ListPool<Component>.Release(components);
//重新填充网格并放入canvasRenderer
s_VertexHelper.FillMesh(workerMesh);
canvasRenderer.SetMesh(workerMesh);
}
//这是vertextHelper的定义部分,Image,RawImage,Text都重写了OnPopulateMesh函数
public class VertexHelper : IDisposable
{
private List<Vector3> m_Positions = ListPool<Vector3>.Get();
private List<Color32> m_Colors = ListPool<Color32>.Get();
private List<Vector2> m_Uv0S = ListPool<Vector2>.Get();
private List<Vector2> m_Uv1S = ListPool<Vector2>.Get();
private List<Vector3> m_Normals = ListPool<Vector3>.Get();
private List<Vector4> m_Tangents = ListPool<Vector4>.Get();
private List<int> m_Indicies = ListPool<int>.Get();
}
对于某些情况,可以实现自己的UI组件并重写OnPopulateMesh函数以提高性能,比如一个UI想要一个透明背景阻挡UI点击穿透到下层。则可自实现一个继承raphic的类型并重写OnPopulateMesh函数,在其中清掉它包含的顶点数据。
合并网格的关键在于CanvasRenderer和Canvas,但并未开源。
Mask
var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial;
var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0);
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = unmaskMaterial;
graphic.canvasRenderer.popMaterialCount = 1;
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);
return m_MaskMaterial;
这是Mask部分代码,它通过实时渲染中的模板方法来裁切不需要的部分,所有在mask下的物体都会被裁切,可以说这一步是在GPU中做的,实现方法是shader中的模板方法。
RectMask2D
public virtual void PerformClipping()
{
// if the parents are changed
// or something similar we
// do a recalculate here
//如果需要重新clip,获取裁切范围
if (m_ShouldRecalculateClipRects)
{
MaskUtilities.GetRectMasksForClip(this, m_Clippers);
m_ShouldRecalculateClipRects = false;
}
// get the compound rects from
// the clippers that are valid
bool validRect = true;
//获取所有的关联RectMask2D的遮罩范围
//计算不需要裁切的部分,其他部分都进行裁切
Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
if (clipRect != m_LastClipRectCanvasSpace)
{
for (int i = 0; i < m_ClipTargets.Count; ++i)
m_ClipTargets.SetClipRect(clipRect, validRect);
m_LastClipRectCanvasSpace = clipRect;
m_LastClipRectValid = validRect;
}
for (int i = 0; i < m_ClipTargets.Count; ++i)
m_ClipTargets.Cull(m_LastClipRectCanvasSpace, m_LastClipRectValid);
}
//对所有需要裁切的UI元素,进行裁切操作
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
if (validRect)
canvasRenderer.EnableRectClipping(clipRect);
else
canvasRenderer.DisableRectClipping();
}
4.5构建UI框架
- 管理类:UI Component,UI Manager,Stack,UI Group,加载,生命周期。
- 实例:基类,抽象共性,处理逻辑,和实际的Go或MonoBhvr拆离。
- 事件响应机制。
- 自定义组件
- 动画
- 音效
- 3D物体跟随
- 滚动页、缓存
- 计数、飘字
4.6优化
动静分离
如果放在一起会将无关的UI元素一并引起UI重绘。
Canvas为节点拆分动与静,把会动的UI元素放入专门为它们准备的合并用的Canvas上,再将静止不动的UI元素留在原来的。
拆分过重UI
随着项目推进,一个UI预设可能会变得越来越复杂,越来越大。
对于这部分,可将其各个子模块拆分出来,或做二次拆分。
&#34;加载和实例化的过程本身是无法规避CPU消耗的,但我们可以将这些CPU消耗分散在一个比较长的时间线上,在此过程中注意权衡加载速度与内存。但是,如果小个体被频繁加载和销毁,也同样会消耗很多CPU。&#34;
预加载
在Preload流程中预加载一些资源。
图集Alpha分离
压缩图集减少包体大小,但可能带来模糊、锯齿。,这是因为在使用ETC或PVRTC时将透明通道一并压进去了,因此需要分离alpha通道。
UGUI内部集成。
字体
拆出常用字。另外生成一个字体文件。
登陆场景:只需要一些字母和数字。
取名:只保留一些常用字。
动态字体、TMP:提取公共字。比如【道可道,非常道】->【道可非常】。
滚屏优化
主要问题:生成和触发滚动,消耗CPU重构Mesh,如果一直移动则每帧重构。
缓存池,只实例化需要显示的实例,移出滚屏范围的实例回收。
一些现成的插件可以解决这类问题,或者自己实现。
“ 我们在窗口中实例化10排元素显示在那里滚动窗口中,其中5排是展示在中央的窗口上的,另外5排中的顶上2排因为超出了窗口被裁剪而无法看见,同样的下面3排也是因为超出了窗口被裁剪无法看见,在整个10排元素整体向上滑动期间,顶上2排变成了3排,底下3排变成了2排,其中最顶上的1排超过了重置的界线,就被移动到了底下去了,这样整体10排元素,变成了顶上2排,底下3排的局面,这样不断反复,不断在移动顶上或底下的1排元素,把他们移动到需要补充的位置上去。看起来像是,很顺畅地上下滚屏整个500个元素那样,实际上是对这5排元素在不断重复的利用而已。”
UI展示和关闭的优化
- 预加载
- 关闭时隐藏
- 移出屏幕
- 设置层级不让相机渲染
对象池、引用池
注意设置合理的expire time
贴图优化
引擎内部使用的是自己的格式,不管外部是PNG还是JPG。
- 是否需要透明通道
- 图集2次方大小的尺寸纠正
- Read/Write会导致运行时额外拷贝一份到内存。
- 去除MipMap
- 选择合适的压缩方式,无压缩->RGBA16->RGB24->RGB16
- 压缩算法,RGB ECT2 4bits和RGB PVRTC 4bits,是不带透明通道的压缩算法。
做好取舍和平衡,更高的压缩倍率意味着更低的图像质量。
内存泄漏
程序:引用计数->互相引用,环形引用
Mono:逐渐抛弃。
IL2CPP:内存依然托管,只是由C++编写VM来接管内存,不过这个VM只是内存托管而已,并不解析和执行任何代码,只是个管理器。
资源:Native内存泄漏,加载后不释放。
MemoryProfiler,内存快照,需要人工比对。
两种寻找资源内存泄漏的技巧:
“1) 通过资源名来识别。即在美术资源(如贴图、材质)命名的时候,就将其所属的游戏状态放在文件名中,如某贴图叫做bg.png,在房间中使用,则修改为Room_bg.png。这样在Profile工具里一坨内存资源里面,混入了一个Room大头的资源,可以很容易地识别出来,也方便利用程序来识别。这个方法但也不是万能的,因为在项目制作过程当中,一张图需要被用到各个场景中去,很可能也不只一两个,有时甚至四五个场景中都会用,只用前缀来代替使用场景的指定,很多时候也会造成另一种误区。甚至由于项目的复杂度扩展到一定程度,包括人员更替,在检查资源泄漏时,用前缀来判断使用场景点不太靠谱,因为你根本就不知道这张图在哪使用了。所以说技巧只能辅助你,并不是说一定能有效。”
“2) 我们可以通过Unity提供的接口Resources.FindObjectsOfTypeAll()进行资源的Dump.可以根据需求Dump贴图、材质、模型或其他资源类型,只需要将Type作为参数传入即可。Dump成功之后我们将这些信息结果保存成一份文本文件,这样可以用对比工具对多次Dump之后的结果进行比较,找到新增的资源,那么这些资源就是潜在的泄漏对象,需要重点追查。”
Unity的项目优化工具:借助UWA的GOT、内存快照比对工具
参考:深入浅出再谈Unity内存泄漏 作者:Arthuryu
高低端机型
- UI贴图质量区分对待,不同设备分两套贴图,一套针对高端机无压缩,一套针对低端机压缩。
- 特效区分对待,低端设备减少特效、例子数量,非关键部分不显示特效。
- 阴影区分对待,针对设备不同使用不同质量或不使用阴影。
- QualitySettings.shadowResolution 设置渲染质量,QualitySettings.shadows 设置有无和模式以及 QualitySettings.shadowProjection 投射质量,QualitySettings.shadowDistance 阴影显示距离,QualitySettings.shadowCascades 接受灯光的数量等。
- 角色底部加一个黑色面片代替阴影。
- 只有部分模型使用实时阴影。
- LOD、抗锯齿
- QualitySettings.masterTextureLimit等
设备区分
- 硬件型号,比如一些固定版本的设备,Apple,或者一些主机,硬件是随着型号固定的,很好判断。
- 对于Android或PC这类平台,就需要综合考虑内存,CPU型号,平均帧率等。
GC优化
触发:1、在堆内存上进行内存分配操作而内存不够的时候都会触发垃圾回收来利用闲置的内存;2、GC会自动的触发,不同平台运行频率不一样;3、 GC可以被强制执行。
策略:1、减少堆内存的分配和引用分配;2、降低分配和回收频率;3、测量GC和堆内存的扩展时间。
一些方案:
- 缓存变量和对象。
- 优化业务逻辑,减少逻辑调用。
- 缓存集合
- 池
- string,缓存,StringBuilder,拆分,Release版本移除Log。
- 尽量避免携程。
- 低版本的Unity注意foreach的问题。
- 尽量避免匿名函数,这会额外生成匿名类和一个静态函数。
- 尽量避免LINQ
- 在适当的时候主动发起GC,比如Preload完成,或者切换场景前后。
|
|