置灰 UI置灰是指某个图标按钮变成灰色。置灰图片的效果还可以,但是置灰文字就不太好了。因为文字类型比较多,例如描边、渐变和阴影等。直接置灰的效果未必好,可以整体修改文字的颜色。如果需要置灰文字,可以和美术人员商量一种颜色。例如将所有的文字改成白色,不过依然得考虑文字描边、阴影的情况。
如下图所示,在UI节点的顶层上挂上UIGray脚本,可以在编辑模式下勾选isGray复选框来预览效果
给当前节点下所有UI元素换成一种带置灰的Shader,还原置灰就是把这些Shader取消即可。
这里我们直接修改Unity默认的材质并设定自定义的shader,省去了在工程中另外设置置灰专用的材质的步骤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 using UnityEngine;using UnityEngine.UI;#if UNITY_EDITOR using UnityEditor;#endif public class UIGray : MonoBehaviour { private bool _isGray = false ; public bool isGray { get { return _isGray; } set { if (_isGray != value ) { _isGray = value ; SetGray(isGray); } } } static private Material _defaultGrayMaterial; static private Material grayMaterial { get { if (_defaultGrayMaterial == null ) { _defaultGrayMaterial = new Material(Shader.Find("Custom/UIGray" )); } return _defaultGrayMaterial; } } void SetGray (bool isGray ) { int count = 0 ; Image[] images = transform.GetComponentsInChildren<Image>(); count = images.Length; for (int i = 0 ; i < count; i++) { Image g = images[i]; if (isGray) { g.material = grayMaterial; } else { g.material = null ; } } } } #if UNITY_EDITOR [CustomEditor(typeof(UIGray)) ] public class UIGrayInspector : Editor { public override void OnInspectorGUI () { base .OnInspectorGUI(); UIGray gray = target as UIGray; gray.isGray = GUILayout.Toggle(gray.isGray, "isGray" ); if (GUI.changed) { EditorUtility.SetDirty(target); } } } #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 Shader "Custom/UIGray" { Properties { [PerRendererData] _MainTex("Sprite Texture", 2 D) = "white" {} _Color("Tint", Color) = (1 ,1 ,1 ,1 ) _StencilComp("Stencil Comparison", Float) = 8 _Stencil("Stencil ID", Float) = 0 _StencilOp("Stencil Operation", Float) = 0 _StencilWriteMask("Stencil Write Mask", Float) = 255 _StencilReadMask("Stencil Read Mask", Float) = 255 _ColorMask("Color Mask", Float) = 15 [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip("Use Alpha Clip", Float) = 0 } SubShader { Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "PreviewType" = "Plane" "CanUseSpriteAtlas" = "True" } Stencil { Ref[_Stencil] Comp[_StencilComp] Pass[_StencilOp] ReadMask[_StencilReadMask] WriteMask[_StencilWriteMask] } Cull Off Lighting Off ZWrite Off ZTest[unity_GUIZTestMode] Blend SrcAlpha OneMinusSrcAlpha ColorMask[_ColorMask] Pass { Name "Default" CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #include "UnityCG.cginc" #include "UnityUI.cginc" #pragma multi_compile __ UNITY_UI_ALPHACLIP struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; float4 worldPosition : TEXCOORD1; UNITY_VERTEX_OUTPUT_STEREO }; fixed4 _Color; fixed4 _TextureSampleAdd; float4 _ClipRect; v2f vert(appdata_t IN) { v2f OUT; UNITY_SETUP_INSTANCE_ID(IN); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); OUT.worldPosition = IN.vertex; OUT.vertex = UnityObjectToClipPos(OUT.worldPosition); OUT.texcoord = IN.texcoord; OUT.color = IN.color * _Color; return OUT; } sampler2D _MainTex; fixed4 frag(v2f IN) : SV_Target { half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color; color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); #ifdef UNITY_UI_ALPHACLIP clip(color.a - 0.001 ); #endif float gray = dot (color.xyz, float3(0.299 , 0.587 , 0.114 )); color.xyz = float3(gray, gray, gray); return color; } ENDCG } } }
粒子特效与UI排序 在Canvas下的UI元素是按照Hierarchy视图下的层级顺序排序的。如果有多个Canvas,每一个Canvas都可以设置自己的Order in Layer。如果只有UI元素,一般可以不用在意这个规则。但是如果两个UI之间需要叠加特效,那就需要重新进行排序了。
如下图所示,在Canvas节点下创建三个UIOrder,分别设置它们的深度为0、1、2,这样特效就被夹在两个UI元素之间了。由于特效可能由多个粒子组成,所以需要遍历所有Particle System,然后一起设置它们的Order in Layer了。
必须将Canvas的Render Mode设为Screen Space-Camera才能正确设置粒子系统的排序
UIOrder
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 using UnityEngine;[AddComponentMenu("UI/UIOrder" ) ] public class UIOrder : MonoBehaviour { [SerializeField ] private int _sortingOrder = 0 ; public int SortingOrder { get { return _sortingOrder; } set { if (_sortingOrder != value ) { _sortingOrder = value ; Refresh(); } } } private Canvas _canvas = null ; public Canvas canvas { get { if (_canvas == null ) { _canvas = gameObject.GetComponent<Canvas>(); if (_canvas == null ) { _canvas = gameObject.AddComponent<Canvas>(); } _canvas.hideFlags = HideFlags.NotEditable; } return _canvas; } } public void Refresh () { canvas.overrideSorting = true ; canvas.sortingOrder = _sortingOrder; foreach (ParticleSystemRenderer particle in transform.GetComponentsInChildren<ParticleSystemRenderer>(true )) { particle.sortingOrder = _sortingOrder; } } #if UNITY_EDITOR private void Reset () { Refresh(); } private void OnValidate () { Refresh(); } #endif }
关于ParticleSystem,新建一个空gameObject(在UI组件里,会自动添加Rect Transform),然后添加Particle System组件,然后在里面的renderer模块里,将Material改为Default Particle。这样粒子就会可见了,然后改改其他的数值
如果在game视图下看不到粒子,就修改camera的位置(Screen Space - Camera模式下不可能看不到的)
Mask & RectMask2D 裁切 在UI父对象上添加RectMask2D组件,会根据父对象的Rect边缘裁切子对象边缘
在UI父对象上添加UI-Mask组件,然后添加Image组件,会根据Image组件裁剪子对象边缘
(Mask组件也是下一帧刷新的,点击一下Show Mask Graphic就好了)
Mask组件很消耗性能,会导致DrawCall增加而且有些UI不能合并,谨慎使用
粒子的裁切 在UI上经常需要添加粒子特效,例如滑动列表中就需要同时考虑UI和特效的裁切。
Mask和RectMask2D只能裁切UI,但是不能裁切特效,不过可以通过Shader剔除裁切部分的特效。
如下图所示,将特效和Image都放在SuperMask下统一进行裁切
只要将粒子系统的Renderer——Order in Layer调高就可以让粒子在前面
新建UISuperMask
脚本,将其挂载在SuperMask上,并再挂载一个image组件,Rect的边缘就是粒子裁切的边缘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 using UnityEngine;using UnityEngine.UI;[DisallowMultipleComponent ] [RequireComponent(typeof(Image)) ] [AddComponentMenu("UI/UISuperMask" ) ] public class UISuperMask : Mask { private float m_LastminX = -1f , m_LastminY = -1f , m_LastmaxX = -1f , m_LastmaxY = -1f ; private float m_MinX = 0f , m_MinY = 0f , m_MaxX = 0f , m_MaxY = 0f ; private Vector3[] m_Corners = new Vector3[4 ]; private Image m_Image; void GetWorldCorners () { if (!Mathf.Approximately(m_LastminX,m_MinX) || !Mathf.Approximately(m_LastminY, m_MinY) || !Mathf.Approximately(m_LastmaxX, m_MaxX) || !Mathf.Approximately(m_LastmaxY, m_MaxY)) { RectTransform rectTransform = transform as RectTransform; rectTransform.GetWorldCorners(m_Corners); m_LastminX = m_MinX; m_LastminY = m_MinY; m_LastmaxX = m_MaxX; m_LastmaxY = m_MaxY; m_MinX = m_Corners[0 ].x; m_MinY = m_Corners[0 ].y; m_MaxX = m_Corners[2 ].x; m_MaxY = m_Corners[2 ].y; } } protected override void OnRectTransformDimensionsChange () { base .OnRectTransformDimensionsChange(); Refresh(); } public void Refresh () { GetWorldCorners(); if (Application.isPlaying) { foreach (var system in transform.GetComponentsInChildren<ParticleSystemRenderer>(true )) { SetRenderer(system); } } } void SetRenderer (Renderer renderer ) { if (renderer.sharedMaterial) { Shader shader = Resources.Load<Shader>("SuperMask/Alpha Blended Premultiply" ); renderer.material.shader = shader; Material m = renderer.material; m.SetFloat("_MinX" , m_MinX); m.SetFloat("_MinY" , m_MinY); m.SetFloat("_MaxX" , m_MaxX); m.SetFloat("_MaxY" , m_MaxY); } } }
裁切的原理就是将裁切的区域告诉Shader,可是粒子特效用到的Shader很多,所以需要动态地更换适合当前粒子裁切的Shader。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 Shader "Custom/Alpha Blended Premultiply" { Properties { _MainTex ("Particle Texture", 2 D) = "white" {} _InvFade ("Soft Particles Factor", Range(0.01 ,3.0 )) = 1.0 _MinX ("Min X", Float) = -10 _MaxX ("Max X", Float) = 10 _MinY ("Min Y", Float) = -10 _MaxY ("Max Y", Float) = 10 } Category { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" } Blend One OneMinusSrcAlpha ColorMask RGB Cull Off Lighting Off ZWrite Off SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_particles #include "UnityCG.cginc" sampler2D _MainTex; fixed4 _TintColor; struct appdata_t { float4 vertex : POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; #ifdef SOFTPARTICLES_ON float4 projPos : TEXCOORD1; #endif float3 vpos : TEXCOORD2; }; float4 _MainTex_ST; float _MinX; float _MaxX; float _MinY; float _MaxY; v2f vert (appdata_t v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); #ifdef SOFTPARTICLES_ON o.projPos = ComputeScreenPos (o.vertex); COMPUTE_EYEDEPTH(o.projPos.z); #endif o.vpos = v.vertex.xyz; o.color = v.color; o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex); return o; } sampler2D_float _CameraDepthTexture; float _InvFade; fixed4 frag (v2f i) : SV_Target { #ifdef SOFTPARTICLES_ON float sceneZ = LinearEyeDepth (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos))); float partZ = i.projPos.z; float fade = saturate (_InvFade * (sceneZ-partZ)); i.color.a *= fade; #endif fixed4 col = i.color * tex2D(_MainTex, i.texcoord) * i.color.a; col.a *= (i.vpos.x >= _MinX ); col.a *= (i.vpos.x <= _MaxX); col.a *= (i.vpos.y >= _MinY); col.a *= (i.vpos.y <= _MaxY); col.rgb *= col.a; return col; } ENDCG } } } }
上述代码中,MinX、MaxX、MinY、MaxY就是裁切的区域。由于Shader中不太建议使用if Else来判断,所以尽可能算出颜色来
Unity内置的Shader:chsxf/unity-built-in-shaders: A comprehensive archive for Unity’s built-in shaders (github.com)
Unity的DIYShader:adrian-miasik/unity-shaders: A bunch of shader examples created in Unity (ShaderGraph & Built-in) 🧙✨ (github.com)
Unity的ParticleEffectForUGUI:GitHub - mob-sakai/ParticleEffectForUGUI: Render particle effect in UnityUI(uGUI). Maskable, sortable, and no extra Camera/RenderTexture/Canvas.
粒子自适应 当分辨率发生变化后,UI上的粒子特效就会有问题。例如,一个贴合按钮周围发光的特效,分辨率发生变化后,就无法贴合按钮了。
我们先搭建一个粒子特效场景,注意Canvas是Scale With Screen Size,分辨率为1136×640,Screen Match Mode为Expand,并设定好UICamera
注意UICamera的设置,打开Orthographic,并且把Size设置为3.2,这个数值是开发分辨率高度的一半除以100,640/2/100
100是根据Canvas组件下的Reference pixels per unit决定的,默认情况下是100
在Particle_GuiYuan和Particle_ZuDui中,各挂载了一个Particle System,每个System有自己的Material,每一个material都是由一个专用贴图,和自定义的Particle_Additive_NoFog Shader组成。两个粒子系统都关闭shape、将Maxparticle设为1,一个使用Texture Sheet Animation,一个使用Color Overtime,再控制一下StartSize和StartLifetime,并且打开Looping和Prewarm就可以做出来了
Particle_Additive_NoFog
的Shader文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 Shader "Particles/Additive NoFog" { Properties { _TintColor ("Tint Color", Color) = (0.5 ,0.5 ,0.5 ,0.5 ) _MainTex ("Particle Texture", 2 D) = "white" {} _InvFade ("Soft Particles Factor", Range(0.01 ,3.0 )) = 1.0 } Category { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" } Blend SrcAlpha One AlphaTest Greater .01 ColorMask RGB Cull Off Lighting Off ZWrite Off Fog { Color (0 ,0 ,0 ,0 ) } BindChannels { Bind "Color", color Bind "Vertex", vertex Bind "TexCoord", texcoord } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest #pragma multi_compile_particles #include "UnityCG.cginc" sampler2D _MainTex; fixed4 _TintColor; struct appdata_t { float4 vertex : POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; }; struct v2f { float4 vertex : POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; }; float4 _MainTex_ST; v2f vert (appdata_t v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.color = v.color; o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex); return o; } float _InvFade; fixed4 frag (v2f i) : COLOR { return 2.0 f * i.color * _TintColor * tex2D(_MainTex, i.texcoord); } ENDCG } } SubShader { Pass { SetTexture [_MainTex] { constantColor [_TintColor] combine constant * primary } SetTexture [_MainTex] { combine texture * previous DOUBLE } } } SubShader { Pass { SetTexture [_MainTex] { combine texture * primary } } } } }
用作粒子的material
新建UIParticleScale
脚本,并将其挂载在ParticleSystem的父对象上(在这里也就是ParticleSuit)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 using System.Collections;using System.Collections.Generic;using UnityEngine;public class UIParticleScale : MonoBehaviour { struct ScaleData { public Transform transform; public Vector3 beginScale; } const float DESIGN_WIDTH = 1136f ; const float DESIGN_HEIGHT = 640f ; private Dictionary<Transform, ScaleData> m_ScaleData = new Dictionary<Transform, ScaleData>(); private void Start () { Refresh(); } void Refresh () { float designScale = DESIGN_WIDTH / DESIGN_HEIGHT; float scaleRate = (float )Screen.width / (float )Screen.height; foreach (var p in transform.GetComponentsInChildren<ParticleSystem>(true )) { if (!m_ScaleData.ContainsKey(p.transform)) { m_ScaleData[p.transform] = new ScaleData() { transform = p.transform, beginScale = p.transform.localScale }; } } foreach (var item in m_ScaleData) { if (scaleRate < designScale) { float scaleFactor = scaleRate / designScale; item.Value.transform.localScale = item.Value.beginScale * scaleFactor; } else { item.Value.transform.localScale = item.Value.beginScale; } } } private void OnTransformChildrenChanged () { Refresh(); } #if UNITY_EDITOR private void Update () { Refresh(); } #endif }
我们在Start
方法中计算原始特效的缩放系数,当屏幕变窄后,重新计算缩放系数,并将其赋值给每个特效。需要在playing模式下查看效果
滑动列表嵌套 我们在Unity中,新建这样一个UI场景
这里的ScrollRectNesting是Canvas,设置和我们之前设置的Canvas一样,注意Camera和Scaler的选择。
父级的ScrollView和子级的ScrollView都把ViewPort和Horizontal Scrollbar以及Vertical Scrollbar删掉,将content作为它们的子级。父级只打开Vertical、子级只打开Horizontal
父级的ScrollView挂载RectMask 2D,并把Content放大一些,子级的Content挂载horizontal layout group和content size fitter,这样才能保证子级的image自动横向排列并且能让我们滑到结尾
这时候我们运行,滑动列表时会发现一个问题:当开始点击位置在子级的ScrollView上时,只能左右滑动
这时候我们继承并重写Scroll Rect组件的部分逻辑,让我们点在子级ScrollView时也能上下滑动
新建CustomScrollRect
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 using UnityEngine;using UnityEngine.UI;using UnityEngine.EventSystems;public class CustomScrollRect : ScrollRect { private CustomScrollRect m_Parent; public enum Direction { Horizontal, Vertical } private Direction m_Direction = Direction.Horizontal; private Direction m_BeginDragDirection = Direction.Horizontal; protected override void Awake () { base .Awake(); Transform parent = transform.parent; if (parent) { m_Parent = parent.GetComponentInParent<CustomScrollRect>(); } m_Direction = this .horizontal ? Direction.Horizontal : Direction.Vertical; } public override void OnBeginDrag (PointerEventData eventData ) { if (m_Parent) { m_BeginDragDirection = Mathf.Abs(eventData.delta.x) > Mathf.Abs(eventData.delta.y) ? Direction.Horizontal : Direction.Vertical; if (m_BeginDragDirection != m_Direction) { ExecuteEvents.Execute(m_Parent.gameObject, eventData, ExecuteEvents.beginDragHandler); return ; } } base .OnBeginDrag(eventData); } public override void OnDrag (PointerEventData eventData ) { if (m_Parent) { if (m_BeginDragDirection != m_Direction) { ExecuteEvents.Execute(m_Parent.gameObject, eventData, ExecuteEvents.dragHandler); return ; } } base .OnDrag(eventData); } public override void OnEndDrag (PointerEventData eventData ) { if (m_Parent) { if (m_BeginDragDirection != m_Direction) { ExecuteEvents.Execute(m_Parent.gameObject, eventData, ExecuteEvents.endDragHandler); return ; } } base .OnEndDrag(eventData); } public override void OnScroll (PointerEventData data ) { if (m_Parent) { if (m_BeginDragDirection != m_Direction) { ExecuteEvents.Execute(m_Parent.gameObject, data, ExecuteEvents.scrollHandler); return ; } } base .OnScroll(data); } }
然后将此脚本分别替换掉原先在ScrollView上的ScrollRect组件,父级只打开Vertical,子级只打开Horizontal
这样就不会有冲突问题了
UI模板嵌套防止保存 Unity并没有提供Prefab的嵌套,有一些UI使用的都是相同的背景图案,要是每个界面都重复放进去,后面改起来非常麻烦。
这个时候我们采用运行时读取的方式,将使用相同背景的UI界面只设置背景预览,而不将背景保存进对应UI的prefab里面
新建UIUnsaveable
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using UnityEngine;public class UIUnsaveable : MonoBehaviour { void Reset () { foreach (var item in GetComponentsInChildren <RectTransform >()) { if (item.gameObject != gameObject) { item.gameObject.hideFlags = HideFlags.DontSave; } } } }
我们可以监听Prefab被保存的事件。凡是设置成HideFlags.DontSave的GameObject,可以在保存Prefab时再清空一下
在Editor文件夹中,新建PrefabSavingListener
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using UnityEngine;using UnityEditor;public class PrefabSavingListener : MonoBehaviour { [InitializeOnLoadMethod ] static void InitializeOnloadMethod () { PrefabUtility.prefabInstanceUpdated = (GameObject go) => { Debug.LogFormat("path:{0}" , AssetDatabase.GetAssetPath(PrefabUtility.GetCorrespondingObjectFromSource(go))); }; } }
UI特效与界面分离 Unity的特效最好不要直接挂在Prefab上,因为特效文件的依赖关系非常复杂,一旦挂在Prefab上,以后构建AssetBundle的时候可能产生很多冗余。
在代码中直接加载是最好的,但是为了预览方便,可以使用上述模板嵌套的方式。
下面我们使用一个ResPreview
脚本,这个脚本可以自动将挂载的prefab生成预览。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using UnityEngine;public class ResPreview : MonoBehaviour { [SerializeField ] private string m_LoadPath = string .Empty; [SerializeField ] private bool m_IsInitLoad = true ; public bool IsLoad { get ; private set ; } private void Awake () { IsLoad = false ; if (m_IsInitLoad) Load(); } public void Load () { GameObject prefab = Resources.Load<GameObject>(m_LoadPath); if (prefab) { GameObject go = Instantiate<GameObject>(prefab); go.transform.SetParent(transform, false ); go.name = prefab.name; #if UNITY_EDITOR foreach (Transform t in go.GetComponentsInChildren<Transform>()) { t.gameObject.hideFlags = HideFlags.NotEditable | HideFlags.DontSave; } #endif } IsLoad = true ; } }
我们自定义一下ResPreview
脚本的Inspector,只序列化保存它的路径,并且实例化特效到Hierarchy视图中来预览
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 using UnityEngine;using UnityEditor;using System.IO;[CanEditMultipleObjects ] [CustomEditor(typeof(ResPreview)) ] public class ResPreviewInspector : Editor { [MenuItem("GameObject/ResPreview" ,false,12) ] static void LoadResPreview () { ResPreview resPreview = new GameObject("ResPreview" ).AddComponent<ResPreview>(); resPreview.transform.SetParent(Selection.activeTransform, false ); Selection.activeTransform = resPreview.transform; } private void OnEnable () { if (target != null ) { if (!Application.isPlaying) { ClearHierarchy(); (target as ResPreview).Load(); } } } public override void OnInspectorGUI () { serializedObject.Update(); GUILayout.Label("请把Resources目录下的Prefab拖入" ); string loadPath = serializedObject.FindProperty("m_LoadPath" ).stringValue; EditorGUI.BeginChangeCheck(); GameObject prefab = Resources.Load<GameObject>(loadPath); GameObject newPrefab = EditorGUILayout.ObjectField("Prefab" , prefab, typeof (GameObject), false ) as GameObject; if (EditorGUI.EndChangeCheck()) { string resPath; bool isResFolder = IsResourcesFolder(newPrefab, out resPath); if (!isResFolder) { EditorUtility.DisplayDialog("提示" , "必须拖拽Resources目录下的Prefab" , "OK" ); ClearHierarchy(); } serializedObject.FindProperty("m_LoadPath" ).stringValue = resPath; if (isResFolder) { serializedObject.ApplyModifiedProperties(); if (!Application.isPlaying) { (target as ResPreview).Load(); } } } serializedObject.FindProperty("m_IsInitLoad" ).boolValue = EditorGUILayout.Toggle("是否Awake加载" , serializedObject.FindProperty("m_IsInitLoad" ).boolValue); GUILayout.Space(18f ); GUILayout.BeginHorizontal(); if (GUILayout.Button("Refresh" , GUILayout.Width(80 ))) (target as ResPreview).Load(); if (GUILayout.Button("Clear" , GUILayout.Width(80 ))) ClearHierarchy(); GUILayout.EndHorizontal(); serializedObject.ApplyModifiedProperties(); } protected bool IsResourcesFolder (Object o,out string resPath ) { if (o) { string path = AssetDatabase.GetAssetPath(o); Debug.LogFormat("开始捕获的路径:{0}" , path); bool beFirst = true ; string tmp = string .Empty; DirectoryInfo dir = new DirectoryInfo(path); while (dir != null ) { if (dir.Name == "Resources" ) { resPath = tmp; return true ; } tmp = tmp.Insert(0 , beFirst ? Path.GetFileNameWithoutExtension(dir.Name) : dir.Name + "/" ); Debug.LogFormat("处理后的路径:{0}" , tmp); if (beFirst) beFirst = false ; Debug.LogFormat("文件夹信息:{0}" , dir.Name); dir = dir.Parent; } } resPath = string .Empty; return false ; } private void ClearHierarchy () { Transform transform = (target as ResPreview).transform; if (transform != null ) { while (transform.childCount > 0 ) { DestroyImmediate(transform.GetChild(0 ).gameObject); } } } }
使用方法:
我们首先必须把我们的UIprefab放在Resources文件夹或者其子文件夹里
在需要预览的UI父组件下右键添加脚本
将resources文件夹内的UIPrefab放在脚本下,勾选Awake,点击运行时会将prefab预览,否则只能在编辑器中预览
输入事件 UGUI提供了InputField类来管理输入事件。
我们首先新建AutoReplaceChar
脚本,这个脚本可以监听输入事件和输入字符,将输入的字符“a”修改成字符“*”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 using UnityEngine;using UnityEngine.UI;public class AutoReplaceChar : MonoBehaviour { public InputField inputField; public Text tips; private void Start () { inputField.onValueChanged.AddListener((string content)=> { tips.text = string .Format("已输入{0}个字符" , content.Length); }); inputField.onValidateInput += (string input, int charIndex, char addedInput) => { if (addedInput == 'a' ) { addedInput = '*' ; } return addedInput; }; } }
然后挂载在游戏场景上,把需要的InputField和Text放进去。
可以看到onValueChanged
必须要加AddListener
方法来传入新的方法,而onValidateInput
本质就是一个delegate,直接+=就可以
注意InputField组件下的placeholder内的text组件,在输入文字后会自动unenable,所以这个AutoReplaceChar
脚本需要挂载的text应该需要自己添加
按钮不规则点击区域 在某些情况下,我们需要自定义按钮的点击形状,而不是统一的方框
新建UIPolygon
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 using UnityEngine;using UnityEngine.UI;#if UNITY_EDITOR using UnityEditor;#endif [RequireComponent(typeof(PolygonCollider2D)) ] public class UIPolygon : Image { private PolygonCollider2D _polygon = null ; private PolygonCollider2D polygon { get { if (_polygon == null ) _polygon = GetComponent<PolygonCollider2D>(); return _polygon; } } protected UIPolygon () { useLegacyMeshGeneration = true ; } protected override void OnPopulateMesh (VertexHelper vh ) { vh.Clear(); } public override bool IsRaycastLocationValid (Vector2 screenPoint, Camera eventCamera ) { return polygon.OverlapPoint(eventCamera.ScreenToWorldPoint(screenPoint)); } #if UNITY_EDITOR protected override void Reset () { base .Reset(); transform.position = Vector3.zero; float w = (rectTransform.sizeDelta.x * 0.5f ) + 0.1f ; float h = (rectTransform.sizeDelta.y * 0.5f ) + 0.1f ; polygon.points = new Vector2[] { new Vector2(-w,-h), new Vector2(w,-h), new Vector2(w,h), new Vector2(-w,h) }; } #endif } #if UNITY_EDITOR [CustomEditor(typeof(UIPolygon),true) ] public class UIPolygonInspector : Editor { public override void OnInspectorGUI () { } } #endif
上述脚本重写了image组件,在判断image组件是否被点击的IsRaycastLocationValid
方法中,返回PolygonCollider2D.OverlapPoint
方法来实现改变按钮形状的目的
使用方法
新建Button,然后将Image和Text的Raycast Target关闭
在Button的子级新建空对象,挂载UIPolygon
脚本
编辑PolygonCollider的边界,注意它要在组件本身Rect框内部
更换默认Shader UI元素不需要指定材质但是却能显示正确,其原因是UGUI提供了默认材质以及Shader。默认是可以兼容任何复杂情况,但是未必是最优方案。
例如,大部分UI是不需要渲染背面的,并且不需要设置颜色。在Shader中删除Cull Off表示剔除UI背景。而在Shader中不再乘以颜色:
1 half4 color = (tex2D(_MainTex,IN.texcoord) + _TextureSampleAdd):
如果修改的Shader还需要绑定到新材质上并赋予每个UI元素,那就太麻烦了,我们希望改掉默认的Shader。
我们从GitHub中下载的Unity Built-in Shader放在项目中进行修改,然后,打开Edit——Project Settings——Graphic——Always Included Shaders
如果有一些UI确实需要渲染背面或者调节颜色,那就单独写一个shader赋予它
小地图优化 小地图一般由美术人员提供一张比较大的图片,程序使用Mask裁切并将其显示在屏幕上。但是Mask根据其原理,依然会产生额外的渲染开销。
其实,修改图片的UV Rect同样可以达到裁切的效果。当角色移动时,动态修改UV的区域,这样就可以保证只渲染一小块
查看UGUI源码 除了Canvas和RectTransForm,其余的UGUI组件都是开源的,编辑模式和运行模式下的都是C#
Unity内置的Shader:chsxf/unity-built-in-shaders: A comprehensive archive for Unity’s built-in shaders (github.com)
Unity的DIYShader:adrian-miasik/unity-shaders: A bunch of shader examples created in Unity (ShaderGraph & Built-in) 🧙✨ (github.com)
Unity的ParticleEffectForUGUI:https://github.com/mob-sakai/ParticleEffectForUGUI
Unity-Technologies/uGUI: Source code for the Unity UI system. (github.com)