UGUI 源码精读1 (UIBehaviour)
源码位置
- Unity 2019.2 之前在 https://github.com/Unity-Technologies/uGUI 里面
- Unity 2019.2 及以后在 Unity 的安装位置,比如我的就在
D:\Unitys\2019.4.11f1\Editor\Data\Resources\PackageManager\BuiltInPackages\com.unity.ugui
里面。
目录结构
可以新建一个项目,然后在 Packages/Unity UI 里面看到目录结构。
Unity UI 的目录结构
类/接口
如何找到一个接口或者类?
因为 UGUI 的源文件是在Package 里面,所以在 VS 里面打开之后都是杂项文件,那么对于我们寻找类与类之间的关系就很不方便。
比如对于 Graphic (定义如下) 而言,想找到 ICanvasElement 所在的文件,然后会发现并不能通过按住 Ctrl 再点击鼠标来定位到该接口声明的位置,而且也没有名为 ICanvasElement 的文件名
public abstract class Graphic
: UIBehaviour,
ICanvasElement
- 方法一:可以在文件管理器中使用
findStr /s "ICanvasElement" *.*
来递归查找所有出现过 ICanvasElement 的语句。 - 方法二:在 VS 2017 里面可以直接将
D:\Unitys\2019.4.11f1\Editor\Data\Resources\PackageManager\BuiltInPackages\com.unity.ugui
拖拽到 VS 的程序集中,就可以正常索引了,但这种方法在 VS 2019 里面不可行。所以 2019 可以参考方法三。 - 方法三:先将
D:\Unitys\2019.4.11f1\Editor\Data\Resources\PackageManager\BuiltInPackages\com.unity.ugui
放到一个和 Assert 同级的目录,比如取名为 "PackageSource",那么目录结构为:-
|——Assert
|——PackageSource
| |——com.unity.ugui -
更改 Packages 下面的 manifest.json 文件,将
"com.unity.ugui": "1.0.0",
变为"com.unity.ugui": "file:C:/Files/Unity/Projects/UGUITest/PackageSource/com.unity.ugui",
-
等待 Unity 和 VS 重新加载就可以在 VS 中看到 UGUI 的代码了
- 完成后的目录结构
-
TODO
第三种方法会导致 create 里面没有 UI 这一栏。
参考
Unity打开Package目录下cs代码显示杂项文件的解决办法
UIBehaviour
位置:RunTime\EventSystem\UIBehaviour.cs
里面有很多虚函数,Unity Monobehaviour 里面可以看到,比如 OnRectTransformDimensionsChange
这种不是很熟悉的,其实是跟 Start
、Update
一样的。下面是源文件,中文注释是自己写的。
*OnRectTransformDimensionsChange
,OnBeforeTransformParentChanged
,OnDidApplyAnimationProperties
是在 Unity Monobehaviour 里面找不到的,合理怀疑是官方文档没有更新,因为 OnBeforeTransformParentChanged() 上面说看 MonoBehaviour.OnBeforeTransformParentChanged
,但 Unity MonoBehaviour 里面并没有这个函数
namespace UnityEngine.EventSystems
{
/// <summary>
/// Base behaviour that has protected implementations of Unity lifecycle functions.
/// </summary>
// 抽象类,虽然里面没有抽象函数,但是申明为抽象类可以防止被实例化
public abstract class UIBehaviour : MonoBehaviour
{
// 虚函数,可以被重写,也可以不被重写,相较于抽象函数,没有强制性(抽象函数子类必须要实现)
protected virtual void Awake()
{}
protected virtual void OnEnable()
{}
protected virtual void Start()
{}
protected virtual void OnDisable()
{}
protected virtual void OnDestroy()
{}
/// <summary>
/// Returns true if the GameObject and the Component are active.
/// </summary>
// MonoBehaviour 里面没有这个函数,所以是从 UIBehaviour 才开始有的。
public virtual bool IsActive()
{
return isActiveAndEnabled;
}
#if UNITY_EDITOR
protected virtual void OnValidate()
{}
protected virtual void Reset()
{}
#endif
/// <summary>
/// This callback is called if an associated RectTransform has its dimensions changed. The call is also made to all child rect transforms, even if the child transform itself doesn't change - as it could have, depending on its anchoring.
/// </summary>
protected virtual void OnRectTransformDimensionsChange()
{}
protected virtual void OnBeforeTransformParentChanged()
{}
protected virtual void OnTransformParentChanged()
{}
protected virtual void OnDidApplyAnimationProperties()
{}
protected virtual void OnCanvasGroupChanged()
{}
/// <summary>
/// Called when the state of the parent Canvas is changed.
/// </summary>
protected virtual void OnCanvasHierarchyChanged()
{}
/// <summary>
/// Returns true if the native representation of the behaviour has been destroyed.
/// </summary>
/// <remarks>
/// When a parent canvas is either enabled, disabled or a nested canvas's OverrideSorting is changed this function is called. You can for example use this to modify objects below a canvas that may depend on a parent canvas - for example, if a canvas is disabled you may want to halt some processing of a UI element.
/// </remarks>
public bool IsDestroyed()
{
// Workaround for Unity native side of the object
// having been destroyed but accessing via interface
// won't call the overloaded ==
return this == null;
}
}
}
现在做一个小实验,也就是在每个函数里面加上 Debug.log("函数名")
,可以看出各个虚函数都是什么时候调用的。
OnValidate()
介绍:当脚本被加载或者 Inspector 面板的值出现变化的时候会被调用,你可以通过它来保证 Inspector 上的一个值固定在一个范围之内,并且这个回调函数只有在编辑器模式下在会被调用,所以使用的时候最好用 #if UNITY_EDITOR
,#endif
包裹住。
#if UNITY_EDITOR
/// <summary>
/// This function is called when the script is loaded or a value is changed in the Inspector (Called in the editor only).
/// You can use this to ensure that when you modify data in an editor, that data stays within a certain range.
/// </summary>
private void OnValidate()
{
Debug.Log("OnValidate");
}
#endif
现象:
- 不管是在编辑模式还是运行模式,启用或者取消启用本脚本,都会被调用。
-
在 Inspector 中修改任意本组件中的值都会被调用
OnValidate.gif
Reset()
介绍:将脚本恢复为默认值。同样的,使用的时候最好用 #if UNITY_EDITOR
,#endif
包裹住。
#if UNITY_EDITOR
/// <summary>
/// Reset to default values.
/// </summary>
private void Reset()
{
Debug.Log("Reset");
}
#endif
现象:
- Inspector 面板,每个脚本右上角的那三个点,有个 reset,点它的时候会被调用 。
-
只有编辑模式可以,运行时不可以
Reset.gif
OnRectTransformDimensionsChange()
介绍:当 RectTransform 发生改变的时候被调用。
/// <summary>
/// This callback is called if an associated RectTransform has its dimensions changed.
/// The call is also made to all child rect transforms, even if the child transform itself doesn't change - as it could have,
/// depending on its anchoring.
/// </summary>
private void OnRectTransformDimensionsChange()
{
Debug.Log("OnRectTransformDimensionsChange");
}
现象:
- 在编辑模式下不会被调用
- 运行模式下,更改 Width,Height,Left,Top,Anchors,Pivot 会被调用
-
修改 Pos X/Y/Z,Rotation,Scale 不会被调用
OnRectTransformDimensionsChange 编辑模式
OnRectTransformDimensionsChange 运行模式
OnBeforeTransformParentChanged() & OnTransformParentChanged()
介绍:当脚本所在的物体的的父物体发生改变,或者父物体的父物体发生改变时会被调用。OnBeforeTransformParentChanged()
发生在 OnTransformParentChanged()
前。可以理解为 Update()
和 LateUpdate()
的关系。
protected virtual void OnBeforeTransformParentChanged()
{
Debug.Log("OnBeforeTransformParentChanged");
}
/// <summary>
/// This function is called when the parent property of the transform of the GameObject has changed.
/// </summary>
protected virtual void OnTransformParentChanged()
{
Debug.Log("OnTransformParentChanged");
}
- 现象1:编辑状态下不会被调用
- 现象2:当父物体或者父物体的父物体,及以上发生改变的时候会被调用,但子物体及以下发生改变不会被调用。(改变是指层级关系,不是 Inspector 的 Transform 组件内容发生改变)
- 现象3:
OnBeforeTransformParentChanged()
永远发生在OnTransformParentChanged()
前。
OnCanvasHierarchyChanged()
介绍:当 Canvas 的可用性发生改变的时候调用。(官方介绍说是父物体的 Canvas 发生变化,但是经实验发现即使该脚本和 Canvas 在同一个物体上也会被调用。)
/// <summary>
/// Called when the state of the parent Canvas is changed.
/// </summary>
protected virtual void OnCanvasHierarchyChanged()
{
Debug.Log("OnCanvasHierarchyChanged");
}
- 现象1:编辑模式下不会被调用
- 现象2:当 自己,父物体,或者父物体的父物体上的 Canvas 组件的可用性发生改变的时候会被调用,子物体身上的 Canvas 发生变化时不会被调用。
- 现象3:Canvas 上的 有些值 发生改变的时候不会被调用,可用性发生变化时一定会被调用(TODO:有些值可能是跟可用性有关的)
- 现象4:当把测试脚本的 勾勾 给 去掉 的时候,该函数依然会在合适的时候被调用。
现象1
现象2
现象3
现象4
OnDidApplyAnimationProperties()
介绍:当物体的属性被 Animation 修改的时候会被调用。
using UnityEngine;
public class Test : MonoBehaviour
{
public int MyInt;
private void Update()
{
this.transform.localScale = new Vector3(MyInt,MyInt,MyInt);
}
protected virtual void OnDidApplyAnimationProperties()
{
Debug.Log("OnDidApplyAnimationProperties");
}
}
现象:
- 当没有 Update() 的时候(也就是不会修改物体的某项属性),通过 Animation 修改 MyInt,不会被调用。
- 当有 Update() 的时候(也就是会修改物体的某项属性),通过 Animation 修改 MyInt,该函数会被调用。
- 必须要用动画去修改当前脚本上的值,并且该值会影响到物体才可以,比如直接用动画修改 Transform,是不会被调用的。
- 编辑模式下在适当的时候也会被调用。这一点可以通过继承自
GridLayoutGroup
然后将代码改为
public class Test : GridLayoutGroup
{
protected override void OnDidApplyAnimationProperties()
{
base.OnDidApplyAnimationProperties();
Debug.Log("OnDidApplyAnimationProperties");
}
}
或者
public class Test : GridLayoutGroup
{
protected new void OnDidApplyAnimationProperties()
{
Debug.Log("OnDidApplyAnimationProperties");
}
}
然后在编辑模式下使用动画修改 Test 脚本的值来测试。
现象1,2 现象2,3
总结:LayoutGroup.OnDidApplyAnimationProperties 虽然也有实现这个函数,但认为它依然是通过 MonoBehavior 来进行回调的,就像 Start() 一样。
OnCanvasGroupChanged()
介绍:当父物体或者自身的 Canvas Group 组件发生变化(包括值改变或者可用性改变)时被调用。
protected virtual void OnCanvasGroupChanged()
{
Debug.Log("OnCanvasGroupChanged");
}
现象:
- 在编辑模式下依然可以被调用。
- 当父物体或者自身的 Canvas Group 组件发生变化(包括值改变或者可用性改变)时被调用。
-
当子物体身上的 Canvas Group 组件发生变化的时候,不会被调用。
OnCanvasGroupChanged.gif
结论
上面这些方法的实验,基本上都是直接或者间接继承了 MonoBehaviour 来完成的,所以我也猜测不管在官方的 MonoBehaviour 里面有没有出现某一个方法,它都是由 MonoBehaviour 来调用的,具体是如何被调用的,可以参考 Unity3d是如何调用MonoBehaviour子类中的Start等方法的?
参考(致谢)
UGUI系統研究講解-----》UIBehaviour功能說明
ICanvasElement
位置:Runtime\UI\Core\CanvasUpdateRegistry.cs
public interface ICanvasElement
{
/// <summary>
/// Rebuild the element for the given stage.
/// </summary>
/// <param name="executing">The current CanvasUpdate stage being rebuild.</param>
void Rebuild(CanvasUpdate executing);
/// <summary>
/// Get the transform associated with the ICanvasElement.
/// </summary>
Transform transform { get; }
/// <summary>
/// Callback sent when this ICanvasElement has completed layout.
/// </summary>
void LayoutComplete();
/// <summary>
/// Callback sent when this ICanvasElement has completed Graphic rebuild.
/// </summary>
void GraphicUpdateComplete();
/// <summary>
/// Used if the native representation has been destroyed.
/// </summary>
/// <returns>Return true if the element is considered destroyed.</returns>
bool IsDestroyed();
}
GraphicRegistry
位置:Runtime\UI\Core\GraphicRegistry.cs
using System.Collections.Generic;
using UnityEngine.UI.Collections;
namespace UnityEngine.UI
{
/// <summary>
/// Registry which maps a Graphic to the canvas it belongs to.
/// </summary>
public class GraphicRegistry
{
private static GraphicRegistry s_Instance;
private readonly Dictionary<Canvas, IndexedSet<Graphic>> m_Graphics = new Dictionary<Canvas, IndexedSet<Graphic>>();
protected GraphicRegistry()
{
// Avoid runtime generation of these types. Some platforms are AOT only and do not support
// JIT. What's more we actually create a instance of the required types instead of
// just declaring an unused variable which may be optimized away by some compilers (Mono vs MS).
// See: 877060
System.GC.KeepAlive(new Dictionary<Graphic, int>());
System.GC.KeepAlive(new Dictionary<ICanvasElement, int>());
System.GC.KeepAlive(new Dictionary<IClipper, int>());
}
/// <summary>
/// The singleton instance of the GraphicRegistry. Creates a new instance if it does not exist.
/// </summary>
public static GraphicRegistry instance
{
get
{
if (s_Instance == null)
s_Instance = new GraphicRegistry();
return s_Instance;
}
}
/// <summary>
/// Associates a Graphic with a Canvas and stores this association in the registry.
/// </summary>
/// <param name="c">The canvas being associated with the Graphic.</param>
/// <param name="graphic">The Graphic being associated with the Canvas.</param>
public static void RegisterGraphicForCanvas(Canvas c, Graphic graphic)
{
if (c == null)
return;
IndexedSet<Graphic> graphics;
instance.m_Graphics.TryGetValue(c, out graphics);
if (graphics != null)
{
graphics.AddUnique(graphic);
return;
}
// Dont need to AddUnique as we know its the only item in the list
graphics = new IndexedSet<Graphic>();
graphics.Add(graphic);
instance.m_Graphics.Add(c, graphics);
}
/// <summary>
/// Dissociates a Graphic from a Canvas, removing this association from the registry.
/// </summary>
/// <param name="c">The Canvas to dissociate from the Graphic.</param>
/// <param name="graphic">The Graphic to dissociate from the Canvas.</param>
public static void UnregisterGraphicForCanvas(Canvas c, Graphic graphic)
{
if (c == null)
return;
IndexedSet<Graphic> graphics;
if (instance.m_Graphics.TryGetValue(c, out graphics))
{
graphics.Remove(graphic);
if (graphics.Count == 0)
instance.m_Graphics.Remove(c);
}
}
private static readonly List<Graphic> s_EmptyList = new List<Graphic>();
/// <summary>
/// Retrieves the list of Graphics associated with a Canvas.
/// </summary>
/// <param name="canvas">The Canvas to search</param>
/// <returns>Returns a list of Graphics. Returns an empty list if no Graphics are associated with the specified Canvas.</returns>
public static IList<Graphic> GetGraphicsForCanvas(Canvas canvas)
{
IndexedSet<Graphic> graphics;
if (instance.m_Graphics.TryGetValue(canvas, out graphics))
return graphics;
return s_EmptyList;
}
}
}