Unity LOD
LOD全称Level of Detail,是Unity提供的一项提高渲染性能的特性。它根据摄像机和对象的距离来渲染对象不同精度的mesh,越远时所需渲染的mesh精度会越低。
Unity中GameObject通过添加LOD Group组件来使用LOD特性。参考:http://docs.unity3d.com/Manual/class-LODGroup.html
LOD Group中可以设置多个level,如LOD:0、LOD:1、LOD:2等。不同level会设置一个百分比,当对象包围盒的高度相对屏幕高度的百分比小于这个百分比时就会进入这个level。
需要注意的是当QualitySettings中LOD Bias的设置不为1时,发生两个level间的过渡,摄像机所在的位置不会是两个level间之前设置好的位置(百分比),而是会有个由LOD Bias决定的偏差。LOD Bias的设置越趋近于0,对象越趋近于Culled。
不同level还可以设置它们的renderers,renderer实际上是一个拥有相应level的mesh的GameObject,通常它就是使用LOD Group组件的对象的孩子节点。
LOD的详细介绍参考:http://docs.unity3d.com/Manual/LevelOfDetail.html
Unity编辑器环境和项目出的手机版本可能会出现LOD表现不一致的情况,通常是由于LOD Bias引起的。Unity的默认QualitySettings在编辑器下会选择Fantastic而出版本会选择Fasttest。
如图Fantastic项上有白色蒙板表示编辑器下选择的品质,而绿色的checkbox表示不同平台出版本时的品质。不同品质下的Lod Bias默认设置会不一样。
Unity各品质的Lod Bias默认值分别为:Fastest 0.3、Fast 0.4、Simple 0.7、Good 1、Beautiful 1.5、Fantastic 2。在编辑器中可以通过LOD Group组件查看当前的LOD Bias的值,如下图。
当出现不同平台LOD表现不一致时记得检查QualitySettings。参考:http://docs.unity3d.com/Manual/class-QualitySettings.html
MonoBehaviour生命周期
UICamera
UICamera是NGUI里很重要的一个脚本,它负责UI事件的通知,是NGUI里UI事件的生产者和投递者。它需要attach到一个绘制NGUI里UI控件的camera上,但它和UI控件的绘制没有任何关系——没有被UICamera attached的camera依然可以正常绘制UI,只是这些UI控件不会收到UI事件的通知。
UICamera支持的UI事件如下。
UICamera本质上是对从Unity Input中获取的输入数据进行了封装和分发。它根据输入设备的不同将输入模式分为了三类,即Mouse(鼠标)、Touch(触摸屏)和Controller(手柄等)。
不同模式下对UI事件的生成会有不同的处理,后面会详细讲到。
UICamera中将事件类型分为World和UI两类,它和eventReceiverMask、rangeDistance一起影响着如何通过raycasts找到输入位置的GameObject。
当EventType为World时UICamera采用GameObject的实际距离判定射线碰撞到的最近的对象,为UI时则采用UIWidget的raycastDepth来判断。eventReceiverMask指定了哪些层能发生和接收UI事件,rangeDistance则指定了能发生和接收UI事件的对象的距离范围。这三者一起决定了UICamera.Raycast方法的执行逻辑和结果。
UICamera中还有一些细节的配置项(成员变量),这里就不一一详述了,下面介绍下UICamera里的核心数据类型——MouseOrTouch。
MouseOrTouch是一个UI事件的抽象,用于保存输入数据中的位置、偏移量等重要信息。
pos:当前帧输入数据的位置信息。
lastPos:前一次输入数据的位置信息,不过好像没用到。
delta:当前帧与上一帧输入数据的位置偏移。
totalDelta:当前帧与事件最开始发生时输入数据的位置偏移,用于drag事件。
pressedCam:发生press事件时的camera。
last:前一次输入位置的对象。
current:当前帧输入位置的对象。
pressed:press的对象。
dragged:drag的对象。
clickTime:click事件分发出去的时间,用于判定double click事件的分发。
clickNotification:OnClick事件的发生条件,None为不发生,Always为总是发生,BasedOnDelta为根据位置移动的偏移量来决定是否发生。
touchBegan:用于Touch模式下标识一个touch是否为began阶段。
pressStarted:标识press事件是否开始。
dragStarted:表示drag事件是否开始。
有了MouseOrTouch,UICamera就通过它来保存不同输入设备对应的输入数据。
mMouse、mTouches、controller就分别对应了鼠标、触摸屏和手柄等的输入数据。鼠标分左键、右键和滚轮,因此由一个包含3个MouseOrTouch对象的数组组成。而触摸屏会有多点触控发生,因此是一个以fingerId为key的Dictionary。
UICamera的事件生成和分发会在每一帧Update时做检测,其主要逻辑如下:
1、处理触摸屏或鼠标事件;
2、通过OnCustomInput的delegate允许使用者自定义的输入处理;
3、处理选中状态的控件;
4、处理键盘和手柄事件;
这里选取触摸屏事件的处理做简单分析:
public void ProcessTouches ()
{
currentScheme = ControlScheme.Touch; // 设置输入模式为Touch
for (int i = 0; i < Input.touchCount; ++i) // 遍历输入数据中的每一个Touch
{
Touch touch = Input.GetTouch(i);
// 获取并设置当前的MouseOrTouch
currentTouchID = allowMultiTouch ? touch.fingerId : 1;
currentTouch = GetTouch(currentTouchID);
// 判断当前的touch状态
bool pressed = (touch.phase == TouchPhase.Began) || currentTouch.touchBegan;
bool unpressed = (touch.phase == TouchPhase.Canceled) || (touch.phase == TouchPhase.Ended);
currentTouch.touchBegan = false;
// 设置当前touch的位置信息
currentTouch.delta = pressed ? Vector2.zero : touch.position – currentTouch.pos;
currentTouch.pos = touch.position;
// 通过Raycast获取hoveredObject并更新touch相关信息
if (!Raycast(currentTouch.pos, out lastHit)) hoveredObject = fallThrough;
if (hoveredObject == null) hoveredObject = genericEventHandler;
currentTouch.last = currentTouch.current;
currentTouch.current = hoveredObject;
lastTouchPosition = currentTouch.pos;
if (pressed) currentTouch.pressedCam = currentCamera;
else if (currentTouch.pressed != null) currentCamera = currentTouch.pressedCam;
// 设置touch的clickTime,用于双击事件的判定
if (touch.tapCount > 1) currentTouch.clickTime = RealTime.time;
// 事件的分发
ProcessTouch(pressed, unpressed);
// 当前touch的事件分发完成后进行重置
if (unpressed) RemoveTouch(currentTouchID);
currentTouch.last = null;
currentTouch = null;
// 不支持多点触控则只处理第一个touch
if (!allowMultiTouch) break;
}
// 没有触控的输入数据时则考虑鼠标等输入设备的输入
if (Input.touchCount == 0)
{
if (useMouse) ProcessMouse();
#if UNITY_EDITOR
else ProcessFakeTouches();
#endif
}
}
从ProcessTouches的处理可以看出真正的事件分发在ProcessTouch里,不仅是这里的触控事件分发,其它如鼠标的事件分发等最终都会调用到它。这里就不再列出全部源码,仅截取一个小片段来分析。
ProcessTouch里当前事件处于pressed状态时,先通过第一个Nofity通知原来处于pressed状态的对象响应OnPress(false)消息,再通过第二个Notify通知当前处于pressed状态的对象响应OnPress(true)消息。而Notify又是如何做到通知对象响应消息的呢?答案是Unity的SendMessage。
这样UICamera就完成了从Unity Input中获取输入数据,再封装成事件,最终又通过Unity SendMessage分发消息的整个闭环流程。
Mesh MeshFilter MeshRenderer
Mesh是计算机图形学中一个很重要的概念,也是Unity中用于渲染的数据模型。
Unity中Mesh继承自Object而非Component,因为它只是一个为MeshFilter、MeshRenderer渲染提供数据的模型。它的主要数据成员如下:
Vector3[] vertices:Mesh的顶点数组。如果resize该数组时其它的顶点属性数组(normals, colors, tangents, UVs)都会自动resize以保持一致。
int[] triangles:Mesh的三角形数组(包括Mesh的sub meshes的三角形)。其中的三角形使用vertices中的索引来表示,数组大小必须是3的倍数(3个顶点索引组成一个三角形)。
Vector2[] uv:顶点贴图的UV数组,大小保持和顶点数组一致。
Color[] colors:顶点的颜色数组,大小保持和顶点数组一致。
Vector3[] normals:顶点的法线数组,大小保持和顶点数组一致。
Vector4[] tangents:顶点的切线数组,大小保持和顶点数组一致。
Unity中渲染Mesh除了需要Mesh本身的数据模型外,还需要MeshFilter和MeshRenderer配合才能完成。
MeshFilter会引用一个Mesh,并将它传递给MeshRenderer完成渲染。它的主要数据成员:
sharedMesh:GameObject关联的Mesh对象。Unity建议通过它只能读取Mesh的数据而不要进行改写,因为改写会同时影响其它使用了该Mesh的GameObject,改写单个GameObject的Mesh可以使用mesh。
mesh:如果MeshFilter没有设置Mesh的话它会生成一个新的Mesh对象;有设置、如果是第一次访问mesh属性的话它会复制生成一个新的Mesh对象,这之后再访问mesh属性都将返回复制出来的Mesh对象。如果访问了mesh属性后原始的sharedMesh丢失了,那么shareMesh返回的将也是mesh。使用mesh属性可以改变单个GameObject的Mesh,但当GameObject被销毁的时候我们需要负责销毁复制生成的mesh。
MeshRenderer继承自Renderer,它从MeshFilter中获取几何信息并在GameObject Transform指定的位置进行渲染。渲染涉及多个方面,这里简要介绍MeshRenderer与材质有关的成员变量:
sharedMaterial:和sharedMesh类似,GameObject关联的Material对象,改写的话会影响到所有使用了该Material的GameObject。当GameObject拥有多个Material时它返回的是sharedMaterials数组里的第一个元素。
sharedMaterials:Unity支持一个GameObject使用多个Material,这种情况下sharedMaterials数组包含了这些Material。
material:和mesh类似,自动生成,GameObject被销毁的时候需要我们负责销毁它。
materials:同material,用于支持多Material。
切换Prefab的保存模式
Unity中prefab的默认保存模式是Mixed的,用文本编辑器打开会出现乱码和部分文本,可以通过Edit—>Project Settings—>Editor将Asset Serialization Mode改为Force Text再保存就能显示全文本了。
Resources.Load和Asset
Resources.Load 加载Resources文件夹里指定路径的asset
AssetBundle 支持通过WWW类来使用流式的assets
Resources.Load从一个默认打进游戏包里的AssetBundle加载资源,而AssetBundle的方式是自己创建、管理AssetBundle文件并从中加载资源,两者本质上没有区别。
AssetBundle的加载:
CreateFromFile从硬盘加载一个AssetBundle文件
CreateFromMemory异步从内存数据创建一个AssetBundle
CreateFromMemoryImmediate同步从内存数据创建一个AssetBundle
AssetBundle加载完毕后只是在内存里创建了AssetBundle的数据结构,此时还没有assets的概念。
Assets的加载:
AssetBundle.Load等加载asset的方法才会从AssetBundle中创建一个asset,这里面可能包括了GameObject、Transform、Texture、Mesh、Material、Shader等各种资源。
当使用asset(prefab) Instantiate出一个对象时,是对这个asset进行复制(clone)加引用的过程:
GameObject、Transform是复制出来的;
Texture、TerrainData是引用原asset的;
Mesh、Material是复制和引用同时存在的。
因此这里需要注意:当Instantiate出的对象未Destory前不能卸载它引用的asset,而当对象Destory后需要通过Resources.UnloadUnusedAssets来释放不再引用的asset。
AssetBundle的卸载:
public void Unload(bool unloadAllLoadedObjects);
unloadAllLoadedObjects为true时将卸载AsserBundle的同时会卸载从AssetBundle中加载出来的assets,如果此时还有对象引用着这些assets时会不安全。
unloadAllLoadedObjects为false时只会卸载AssetBundle,要卸载从AssetBundle中加载出来的assets时需要在合适的时机调用Resources.UnloadUnusedAssets。
UIButton Normal状态颜色不对的问题
今天试验一个方案的时候遇到了一个UIButton的问题:按钮显示出来的时候Normal状态的颜色被设置成了Color.grey。正常情况下应该是Color.white,之前也从未遇到过这个问题,觉得挺奇怪的。
开始怀疑是不是代码里调用了设置按钮disable状态的方法,或者播放了TweenColor动画,但是经过一番搜索后无果。于是想着新建一个空的场景,里面只创建一个UIButton,排除其它因素的影响来看看它的表现是否正常。结果也是编辑器的状态下显示正常,而Play的时候TweenColor自动将其颜色由Color.white变为了Color.grey。
最后只能拿出UIButton的源码查看并调试了。UIButton的源码里有一个UpdateColor方法,它的作用正是通过TweenColor动画改变按钮颜色。
选择在此处下断,发现UIButton在OnEnable的时候确实改变了按钮的颜色。
为什么会改变按钮颜色呢?原来OnEnable里判断isEnabled为false了,而isEnabled为false的情况有三种:1、UIButton为禁用状态;2、未设置collider;3、collider为禁用状态。
检查自己创建按钮的过程,为了快速试验方案只是创建了一个GameObject绑定上UISprite和UIButton,确实忘了设置collider。问题原因找到了,但仔细想想UIButton这里的细节可以做些更人性化的改动。因为通常情况下UI加入按钮肯定是要响应点击等事件的,而UIButton完成这些任务又必须要collider来配合,所以对于UIButton里collider未设置的情况可以输出一个warning来提醒使用者。
收藏一本3D入门的电子书
NGUI源码剖析之UIRect
UIRect是一个代表UI矩形区的抽象类。一个矩形区有上、下、左、右四边,因此UIRect中有4个锚点分别指示这四边anchor的位置:
public AnchorPoint leftAnchor = new AnchorPoint();
public AnchorPoint rightAnchor = new AnchorPoint(1f);
public AnchorPoint bottomAnchor = new AnchorPoint();
public AnchorPoint topAnchor = new AnchorPoint(1f);
有bottom-left、top-left、top-right、bottom-right四个角:
public abstract Vector3[] localCorners { get; }
public abstract Vector3[] worldCorners { get; }
每个矩形区是一个UI的容器,因此UIRect可以组成一个树状结构:
BetterList<UIRect> mChildren即孩子节点列表,OnInit、OnDisable及ParentHasChanged时将进行动态的Add和Remove。
UIRect mParent即父节点,会从该UIRect绑定的GameObject进行回溯寻找,bool mParentFound表示其有没有进行过回溯寻找。
NGUI中UIRoot是所有UI对象的根节点,UIRect中的UIRoot mRoot就指向了该节点,bool mRootSet表示其有没有进行过回溯寻找。
UIRect里为了加速访问绑定的GameObject及其Transform做了缓存:
protected GameObject mGo;
protected Transform mTrans;
public GameObject cachedGameObject { get { if (mGo == null) mGo = gameObject; return mGo; } }
public Transform cachedTransform { get { if (mTrans == null) mTrans = transform; return mTrans; } }
Camera mMyCam为负责渲染UIRect绑定的GameObject这一层的Camera,bool mAnchorsCached表示其以及4个锚点的数据有没有设置好。
bool mStarted:UnityEngine是否回调了Start方法
bool mChanged:父对象中有值(如alpha)改变时将其设置为true,用于刷新表现。
int mLastInvalidate:通过Time.frameCount记录下最后调用Invalidate的帧数。
int mUpdateFrame:通过Time.frameCount记录已经Update过的帧数。
UIRect中的主要功能在Update里:
1、通过mAnchorsCached判断是否需要更新锚点的相关数据,需要则通过ResetAnchors来更新数据。
2、通过mUpdateFrame与Time.frameCount判断该帧是否已经经过Update处理,处理过则本次Update结束了。
3、分别判断4个锚点是否有设置,有的话则判断该锚点的rect是否已经经过Update处理,未处理过则调用其Update方法。
4、判断4个锚点是否至少有1个有设置,则调用OnAnchor方法。
5、调用OnUpdate方法继续处理。
OnAnchor和OnUpdate方法均是抽象方法,需要由UIRect的子类自己来实现:
protected abstract void OnAnchor ();
protected virtual void OnUpdate () { }