置灰

UI置灰是指某个图标按钮变成灰色。置灰图片的效果还可以,但是置灰文字就不太好了。因为文字类型比较多,例如描边、渐变和阴影等。直接置灰的效果未必好,可以整体修改文字的颜色。如果需要置灰文字,可以和美术人员商量一种颜色。例如将所有的文字改成白色,不过依然得考虑文字描边、阴影的情况。

如下图所示,在UI节点的顶层上挂上UIGray脚本,可以在编辑模式下勾选isGray复选框来预览效果

UI设定

UIGray

给当前节点下所有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);//设置为脏后,在Unity中Ctrl+Z将不会恢复此脚本
}
}
}
#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", 2D) = "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
//-----------------add-------------------------
float gray = dot(color.xyz, float3(0.299, 0.587, 0.114));
color.xyz = float3(gray, gray, gray);
//-----------------add-------------------------
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才能正确设置粒子系统的排序

UI排序效果

UIOrder

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")]//放在Add Component按钮——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的Sorting Order
canvas.sortingOrder = _sortingOrder;
foreach (ParticleSystemRenderer particle in transform.GetComponentsInChildren<ParticleSystemRenderer>(true))//true表示disable的组件也算
{
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边缘裁切子对象边缘

RectMask2D

在UI父对象上添加UI-Mask组件,然后添加Image组件,会根据Image组件裁剪子对象边缘

UI——Mask

(Mask组件也是下一帧刷新的,点击一下Show Mask Graphic就好了)

Mask组件很消耗性能,会导致DrawCall增加而且有些UI不能合并,谨慎使用

粒子的裁切

在UI上经常需要添加粒子特效,例如滑动列表中就需要同时考虑UI和特效的裁切。

Mask和RectMask2D只能裁切UI,但是不能裁切特效,不过可以通过Shader剔除裁切部分的特效。

如下图所示,将特效和Image都放在SuperMask下统一进行裁切

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)//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);
}
}
}

挂载SuperMask

裁切的原理就是将裁切的区域告诉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", 2D) = "white" {}
_InvFade ("Soft Particles Factor", Range(0.01,3.0)) = 1.0
//-------------------add----------------------
_MinX ("Min X", Float) = -10
_MaxX ("Max X", Float) = 10
_MinY ("Min Y", Float) = -10
_MaxY ("Max Y", Float) = 10
//-------------------add----------------------
}

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
//-------------------add----------------------
float3 vpos : TEXCOORD2;
//-------------------add----------------------
};

float4 _MainTex_ST;
//-------------------add----------------------
float _MinX;
float _MaxX;
float _MinY;
float _MaxY;
//-------------------add----------------------
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
//-------------------add----------------------
o.vpos = v.vertex.xyz;
//-------------------add----------------------
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;
//-------------------add----------------------
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;
//-------------------add----------------------
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", 2D) = "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
}

// ---- Fragment program cards
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;
// #ifdef SOFTPARTICLES_ON
// float4 projPos : TEXCOORD1;
// #endif
};

float4 _MainTex_ST;

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.color = v.color;
o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}

//sampler2D _CameraDepthTexture;
float _InvFade;

fixed4 frag (v2f i) : COLOR
{
// #ifdef SOFTPARTICLES_ON
// float sceneZ = LinearEyeDepth (UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos))));
// float partZ = i.projPos.z;
// float fade = saturate (_InvFade * (sceneZ-partZ));
// i.color.a *= fade;
// #endif

return 2.0f * i.color * _TintColor * tex2D(_MainTex, i.texcoord);
}
ENDCG
}
}

// ---- Dual texture cards
SubShader {
Pass {
SetTexture [_MainTex] {
constantColor [_TintColor]
combine constant * primary
}
SetTexture [_MainTex] {
combine texture * previous DOUBLE
}
}
}

// ---- Single texture cards (does not do color tint)
SubShader {
Pass {
SetTexture [_MainTex] {
combine texture * primary
}
}
}
}
}

用作粒子的material

使用自定义的Shader

新建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;
}
}
}

///<summary>
///子节点变化时重新刷新深度
/// </summary>
private void OnTransformChildrenChanged()
{
Refresh();
}
#if UNITY_EDITOR
//编辑模式下修改分辨率后在Update中刷新,方便预览效果
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
{
//父CustomScrollRect对象
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)
{
//eventData.delta表示指针当前位置与上一帧位置的偏移值,返回Vector2
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里面

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()
{
//监听prefab被保存事件
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;

//---------序列化信息面板----------
///<summary>
///资源是否已经完成加载
/// </summary>
public bool IsLoad { get; private set; }

private void Awake()
{
IsLoad = false;
if (m_IsInitLoad)
Load();
}

///<summary>
///加载资源
/// </summary>
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>())//GetComponentInChildren是会返回对象本身的
{
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);//返回的是Assets/Resources/UIPrefabs/someUI.prefab
Debug.LogFormat("开始捕获的路径:{0}", path);
bool beFirst = true;
string tmp = string.Empty;
DirectoryInfo dir = new DirectoryInfo(path);
while(dir != null)
{
if(dir.Name == "Resources")//第一次是someUI.prefab,必定不会执行,可见Unity的Prefab在C#中被认为是文件夹
{
resPath = tmp;
return true;
}

tmp = tmp.Insert(0, beFirst ? Path.GetFileNameWithoutExtension(dir.Name) : dir.Name + "/");//第一次执行,返回someUI,第二次执行,返回UIPrefabs/someUI
Debug.LogFormat("处理后的路径:{0}", tmp);
if (beFirst) beFirst = false;
Debug.LogFormat("文件夹信息:{0}", dir.Name);
dir = dir.Parent;//第一次返回UIPrefabs,第二次才返回Resources
}
}
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);
}
}
}
}

使用方法:

  1. 我们首先必须把我们的UIprefab放在Resources文件夹或者其子文件夹里

文件要求

  1. 在需要预览的UI父组件下右键添加脚本

挂载脚本

  1. 将resources文件夹内的UIPrefab放在脚本下,勾选Awake,点击运行时会将prefab预览,否则只能在编辑器中预览

    Inspector

输入事件

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方法来实现改变按钮形状的目的

使用方法

  1. 新建Button,然后将Image和Text的Raycast Target关闭

  2. 在Button的子级新建空对象,挂载UIPolygon脚本

    UIPolygon

  3. 编辑PolygonCollider的边界,注意它要在组件本身Rect框内部

    使用方法

更换默认Shader

UI元素不需要指定材质但是却能显示正确,其原因是UGUI提供了默认材质以及Shader。默认是可以兼容任何复杂情况,但是未必是最优方案。

例如,大部分UI是不需要渲染背面的,并且不需要设置颜色。在Shader中删除Cull Off表示剔除UI背景。而在Shader中不再乘以颜色:

1
half4 color = (tex2D(_MainTex,IN.texcoord) + _TextureSampleAdd)://*IN.color;

如果修改的Shader还需要绑定到新材质上并赋予每个UI元素,那就太麻烦了,我们希望改掉默认的Shader。

我们从GitHub中下载的Unity Built-in Shader放在项目中进行修改,然后,打开Edit——Project Settings——Graphic——Always Included Shaders

默认Shader位置

如果有一些UI确实需要渲染背面或者调节颜色,那就单独写一个shader赋予它

小地图优化

小地图一般由美术人员提供一张比较大的图片,程序使用Mask裁切并将其显示在屏幕上。但是Mask根据其原理,依然会产生额外的渲染开销。

其实,修改图片的UV Rect同样可以达到裁切的效果。当角色移动时,动态修改UV的区域,这样就可以保证只渲染一小块

RawImage

查看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)