FishPlayer

一个喜欢摸鱼的废物

0%

UGUI 一些自用UI表现件基础

Temp

自从开始加班以后就没什么干劲了。这段时间重头工作就是接用户SDK然后补以前落下的一些细节需求。

比较蚌埠住的是,这段时间发现了自己写的很多重复的UI组件代码。而且在这个时候已经不方便再整合替换了,整合替换可能会导致爆炸。

为了尽可能杜绝此类事情发生,决定先把用到的一些基础代码总结一下写在这里,方便下次直接过来抄而不是再手搓了。

上代码

PrimitiveBehaviour(基本结构)

首先定义表现件的最基本的结构。实质就是一个接收值的更新。
此外还多了一个标志,可用于调整该组件是否在第一次收到值得时候使用快进动画。
因为该功能在实际使用的时候,是根据美术的要求来决定的,所以我觉得不写在代码里而是开放出来供直接在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
43
44
45
46
47
48
49
50
51
52
53
54

namespace Core.UI
{
// where T : IComparable, IComparable<T>, IConvertible, IEquatable<T> 其实就是为了限制 T 必须是这些基础类型,但是感觉还是没必要这么写
public interface IBehaviourDispatcher<T> where T : IComparable, IComparable<T>, IConvertible, IEquatable<T>
{
void DispatchValueChange(T nextValue, bool instant);
}

public abstract class PrimitiveBehaviour<T> : MonoBehaviour where T : IComparable, IComparable<T>, IConvertible, IEquatable<T>
{
[SerializeField]
private bool m_instantApplyInitValue = true;

private bool m_hasInit = false;
private T m_currentValue;
private T m_previousValue;

public T currentValue => m_currentValue;
public T previousValue => m_previousValue;
public bool hasInited => m_hasInit;

/// <summary>
/// 持有数据的一方主动调用该方法,以对表现进行更新
/// </summary>
/// <param name="nextValue"></param>
/// <param name="instant">instant changing value can also used to fast forward anim</param>
public void ChangeValue(T nextValue, bool instant)
{
if (m_hasInit)
{
if (!instant && IsValueEqual(currentValue, nextValue))
{
return;
}
m_previousValue = m_currentValue;
m_currentValue = nextValue;
NotifyValueChanged(nextValue, instant);
return;
}

instant = instant || m_instantApplyInitValue;
NotifyValueChanged(nextValue, instant);
m_previousValue = m_currentValue = nextValue;
m_hasInit = true;
}

protected bool IsValueEqual(T l, T r) => 0 == l.CompareTo(r);

protected abstract void NotifyValueChanged(T nextValue, bool instant);

}

}

BoolBehaviour

需要下发数据的一方需要持有一个 BoolBehaviourDispatcher 变量,并通过其把数据变更下发出去。

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

namespace Core.UI
{
[Serializable]
public struct BoolBehaviourDispatcher : IBehaviourDispatcher<bool>
{
[SerializeField]
private BoolBehaviour[] m_behaviours;

public void DispatchValueChange(bool nextValue, bool instant)
{
int count = m_behaviours.Length;
for (int i = 0; i < count; i++)
{
BoolBehaviour behaviour = m_behaviours[i];
if (behaviour.gameObject.activeInHierarchy && behaviour.enabled)
{
behaviour.ChangeValue(nextValue, instant);
}
}
}

#if UNITY_EDITOR
[ContextMenu("FindInChildren")]
private void FindInChildren(GameObject source)
{
var behaviors = source.GetComponentsInChildren<BoolBehaviour>();
m_behaviours = behaviors;
}
#endif

}

public abstract class BoolBehaviour : PrimitiveBehaviour<bool>
{
#if UNITY_EDITOR
[Header("Editor preview"), NonReorderable]
public bool previewValue_editor = false;
public bool previrwInstant = true;

[ContextMenu("SetValuePreview")]
public void SetValuePreview()
{
if (Application.isPlaying)
{
NotifyValueChanged(previewValue_editor, previrwInstant);
return;
}
NotifyValueChanged(previewValue_editor, true);
}
#endif
}

}

FloatBehaviour

需要下发数据的一方需要持有一个 FloatBehaviourDispatcher 变量,并通过其把数据变更下发出去。

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

namespace Core.UI
{
[Serializable]
public struct FloatBehaviourDispatcher: IBehaviourDispatcher<float>
{
[SerializeField]
private FloatBehaviour[] m_behaviours;

public void DispatchValueChange(float nextValue, bool instant)
{
int count = m_behaviours.Length;
for (int i = 0; i < count; i++)
{
var behaviour = m_behaviours[i];
if (behaviour.gameObject.activeInHierarchy && behaviour.enabled)
{
behaviour.ChangeValue(nextValue, instant);
}
}
}

#if UNITY_EDITOR
[ContextMenu("FindInChildren")]
private void FindInChildren(GameObject source)
{
var behaviors = source.GetComponentsInChildren<FloatBehaviour>();
m_behaviours = behaviors;
}
#endif

}

public abstract class FloatBehaviour : PrimitiveBehaviour<float>
{
#if UNITY_EDITOR
[Header("Editor preview"), NonReorderable]
public float previewValue_editor = 1f;
public bool previrwInstant = true;

[ContextMenu("SetValuePreview")]
public void SetValuePreview()
{
if (Application.isPlaying)
{
NotifyValueChanged(previewValue_editor, previrwInstant);
return;
}
NotifyValueChanged(previewValue_editor, true);
}
#endif
}

}

IntBehaviour

需要下发数据的一方需要持有一个 IntBehaviourDispatcher 变量,并通过其把数据变更下发出去。
IntBehaviour 我定义成一个抽象类。因为实际使用的时候(我暂时遇到的情况),在UI表现上会需要有持有一个数值,数值相同不同时的表现,或是UI收到不同的数值再有不同的表现。
所以这边分做成了两个 Beheaviour 。

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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

namespace Core.UI
{
[Serializable]
public struct IntBehaviourDispatcher : IBehaviourDispatcher<int>
{
[SerializeField]
private IntBehaviour[] m_behaviours;

public void DispatchValueChange(int nextValue, bool instant)
{
int count = m_behaviours.Length;
for (int i = 0; i < count; i++)
{
var behaviour = m_behaviours[i];
if (behaviour.gameObject.activeInHierarchy && behaviour.enabled)
{
behaviour.ChangeValue(nextValue, instant);
}
}
}

#if UNITY_EDITOR
[ContextMenu("FindInChildren")]
private void FindInChildren(GameObject source)
{
var behaviors = source.GetComponentsInChildren<IntBehaviour>();
m_behaviours = behaviors;
}
#endif

}

public abstract class IntBehaviour : PrimitiveBehaviour<int>
{
#if UNITY_EDITOR
[Header("Editor preview"), NonReorderable]
public int previewValue_editor = 0;
public bool previrwInstant = true;

[ContextMenu("SetValuePreview")]
public void SetValuePreview()
{
if (Application.isPlaying)
{
NotifyValueChanged(previewValue_editor, previrwInstant);
return;
}
NotifyValueChanged(previewValue_editor, true);
}
#endif
}

// 根据收到的值不同 而作不同表现
public abstract class IntSwitchBehaviour<T> : PrimitiveBehaviour<int>
{
[Serializable]
public struct IntPack
{
public int intValue;
public T targetValueDataPack;
}

[Header("Extra param")]
[SerializeField]
private bool m_hasDefaultPack = false;
[SerializeField]
private int m_defaultPackIndex = 0;
[SerializeField]
private bool m_unapplyPrevPack = true;
[SerializeField]
private IntPack[] m_packArray;

protected sealed override void NotifyValueChanged(int nextValue, bool instant)
{
int length = m_packArray.Length;
int meetIndex = -1;
IntPack pack;
for (int i = 0; i < length; i++)
{
pack = m_packArray[i];
if (pack.intValue == nextValue)
{
meetIndex = i;
continue;
}
else if (m_unapplyPrevPack)
{
UnApplyPack(pack.targetValueDataPack);
}
}
if (meetIndex > 0) // has meet
{
pack = m_packArray[meetIndex];
ApplyPack(pack.targetValueDataPack, instant);
return;
}
// apply default one
pack = m_packArray[m_defaultPackIndex];
ApplyPack(pack.targetValueDataPack, instant);
}

protected abstract void ApplyPack(T pack, bool instant);
protected abstract void UnApplyPack(T pack);
}

// 持有一个数值,判断传进来的值是否相等,再做表现
public abstract class IntEqualBehaviour : IntBehaviour
{
[SerializeField]
private int m_targetValue;

private bool m_isPreviousValueEqual = false;

protected sealed override void NotifyValueChanged(int nextValue, bool instant)
{
bool isEqual = IsValueEqual(nextValue, m_targetValue);
bool hasChanged = isEqual != m_isPreviousValueEqual;
if (hasInited)
{
if (hasChanged)
{
NotifyValueEqual(isEqual, instant);
}
return;
}
NotifyValueEqual(isEqual, instant);
}

protected abstract void NotifyValueEqual(bool equal, bool instant);

}
}

Sample

下面是一个 FloatBehaviour 和 BoolBehaviour 的例子。
只需要收到数值,FloatBehaviour 启动tween去修改 Image 的填充/ BoolBehaviour 去开关物体。其它还会用到的表现还有更改颜色,开关物体之类的,
当然实际使用的情况还得考虑 target 是否会无效,以及物体从来没开过,却调用 NotifyValueChanged 之类的细节问题的。

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

namespace Core.UI
{
[RequireComponent(typeof(Image))]
public class SlicesImageFill : FloatBehaviour
{
[Header("Fields")]
[SerializeField]
private Image m_target;
[SerializeField]
private bool m_reverse = false;
[SerializeField]
private Vector2 m_range = new Vector2(0, 1f);
[SerializeField]
private bool m_doTweenFill = false;
[SerializeField]
private TweenParameter m_tweenParameter;

private Tween m_currentTween;

protected override void NotifyValueChanged(float nextValue, bool instant)
{
m_currentTween?.Kill();
float targetFill = CalculateFillValue(nextValue);
if (instant || !m_doTweenFill)
{
m_target.fillAmount = targetFill;
return;
}
m_currentTween = m_target.DOFillAmount(targetFill, m_tweenParameter.duration);
m_currentTween.ApplyParam(m_tweenParameter);
}

private float CalculateFillValue(float rawValue)
{
return Mathf.Lerp(m_range.x, m_range.y, m_reverse ? 1 - rawValue : rawValue);
}

private void OnDestroy()
{
m_currentTween?.Kill();
}

#if UNITY_EDITOR
private void Reset()
{
m_target = GetComponent<Image>();
}
#endif
}

public class BoolReactorObjectActive : BoolBehaviour
{
[Serializable]
public struct ObjectActivePack
{
public GameObject target;
public bool inverse;
}

[Header("Extra param")]
[SerializeField]
private ObjectActivePack[] m_packs;

protected override void NotifyValueChanged(bool value, bool instant)
{
for (int i = 0, length = m_packs.Length; i < length; i++)
{
ObjectActivePack pack = m_packs[i];
pack.target.SetActive(pack.inverse ? !value : value);
}
}
}

}

总结