这阵子项目中需要用到一种特殊样式的血条。描述如下:
1. 正常颜色为红色。受到伤害后,即将扣除的血量变暗(暗红色),并有下降动画效果;
2. 加护盾效果后,增加一部分血量值,该额外部分为白色,护盾效果消失后该部分血量瞬间消失;
3. 在护盾效果下受到伤害时,首先扣除白色血量。白色血量不足扣除时,余下部分从红色血量中扣除;
4. 白色血量的扣除效果为变为灰色并有下降动画效果;
4. 当加护盾效果时,若即将添加的白色血量将使总血条“溢出”,从新计算百分比并排满血条;
5. 中毒时,将相应的血量(按照伤害扣血优先级,即先扣除护盾,再扣除正常)变为紫色。该紫色血量有递减动画;
6. 中毒时若受到伤害,不扣除紫色部分血量(实际上该部分已扣除,但有个缓冲时间),而是红色或白色部分;
7. 若中毒时受到护盾效果?
8. 血条会自动隐藏,血量产生变化时会自动显示;
制作普通血条时,我们一般会用UISlider。
但是这里涉及到护盾和中毒的效果,用UISlider显然是不够的。我首先想到的是用多个血条叠加在一起,分辨为正常血条、中毒血条、护盾血条。但是掉血效果要怎么解决?
如果只是有下降动画,那很好解决,可是会先变暗,这显然是一个slider做不到的。
于是我灵机一动想到了:一个血条,多个UISlider!我们可以写一个自定义血条,该血条包含正常血量、中毒值、护盾值,以及相应的状态属性。
经过实践,果然我的想法是对的。先来看下效果图:
1.掉血效果
2.加护盾
2.1 加护盾时掉血
3. 中毒
复杂的叠加效果我们稍后再讨论。第一步,先完成UI上的结构设计:
1. Heathbar为金色的边框(UISprite)
2. Blank为底色(灰)(UISprite)
3. ShieldedDmg为加护盾时的减血底色(深灰色)(UISprite, UISlider)
4. Shielded为护盾颜色(白色)(UISprite, UISlider)
5. Poisoned为中毒颜色(紫色)(UISprite, UISlider)
6. NormalDmg为正常情况下的减血底色(暗红色)(UISprite, UISlider)
7. Normal为正常血条的颜色(红色)(UISprite, UISlider)
8. thumb为血条末端的小刻度(白色)(UISprite),并设置Normal上Slider的Thumb为它
如此,我们就完成了初步的UI设计。数一下,一共有5个Slider。我们再添加一个名为UIHealthbar自定义脚本,用来管理这些UISlider的数值变化,以及处理相关逻辑。
将UIHealthbar绑到Heathbar上。初步脚本如下:
1 using System;
2 using UnityEngine;
3
4 public class UIHealthbar : MonoBehaviour
5 {
6 #region
7
8 private UISlider _normal;
9 private UISlider _normalDmg;
10 private UISlider _shielded;
11 private UISlider _shieldedDmg;
12 private UISlider _poisoned;
13 private UISprite _barSprite;
14
15 #endregion
16
17
18 /// <summary>
19 /// 全局动画时长
20 /// </summary>
21 private const float AnimDuration = 0.2f;
22
23 /// <summary>
24 /// 渐变类型
25 /// </summary>
26 private const iTween.EaseType EaseType = iTween.EaseType.linear;
27
28 /// <summary>
29 /// 是否正在隐藏或显示(但如或淡出)
30 /// </summary>
31 private bool _isFading;
32
33 /// <summary>
34 /// 用来判断自动隐藏的计时器
35 /// </summary>
36 private float _timer;
37
38 /// <summary>
39 /// 是否自动隐藏
40 /// </summary>
41 public bool autoHide = true;
42
43 /// <summary>
44 /// 是否受到正常伤害
45 /// </summary>
46 private bool IsNormalDamaging
47 {
48 get { return _normalDmg.gameObject.activeSelf; }
49 set { _normalDmg.gameObject.SetActive(value); }
50 }
51
52 /// <summary>
53 /// 是否在加护盾的情况下受到伤害
54 /// </summary>
55 private bool IsShieldedDamaging
56 {
57 get { return _shieldedDmg.gameObject.activeSelf; }
58 set { _shieldedDmg.gameObject.SetActive(value); }
59 }
60
61 /// <summary>
62 /// 是否正在掉血
63 /// </summary>
64 public bool IsDamaging
65 {
66 get { return IsShieldedDamaging || IsNormalDamaging; }
67 }
68
69 /// <summary>
70 /// 是否中毒
71 /// </summary>
72 public bool IsPoisoned
73 {
74 get { return _poisoned.gameObject.activeSelf; }
75 private set { _poisoned.gameObject.SetActive(value); }
76 }
77
78 /// <summary>
79 /// 是否受护盾
80 /// </summary>
81 public bool IsShielded
82 {
83 get { return _shielded.gameObject.activeSelf; }
84 private set { _shielded.gameObject.SetActive(value); }
85 }
86
87 /// <summary>
88 /// 是否可见(自动隐藏相关隐藏)
89 /// </summary>
90 private bool IsVisible
91 {
92 get
93 {
94 throw
95 new NotImplementedException();
96 }
97 set
98 {
99
100 }
101 }
102
103 private void OnEnable()
104 {
105 IsPoisoned = false;
106 IsShielded = false;
107 IsShieldedDamaging = false;
108 IsNormalDamaging = false;
109 }
110
111 private void Awake()
112 {
113 _normal = transform.FindChild("Normal").GetComponent<UISlider>();
114 _normalDmg = transform.FindChild("NormalDmg").GetComponent<UISlider>();
115 _shielded = transform.FindChild("Shielded").GetComponent<UISlider>();
116 _shieldedDmg = transform.FindChild("ShieldedDmg").GetComponent<UISlider>();
117 _poisoned = transform.FindChild("Poisoned").GetComponent<UISlider>();
118 _barSprite = transform.GetComponent<UISprite>();
119 }
120
121 #region 逻辑处理
122
123 /// <summary>
124 /// 加伤害
125 /// </summary>
126 /// <param name="percent">将造成的伤害百分比(小于1)</param>
127 /// <returns>剩余血量百分比</returns>
128 public float AddDamage(float percent)
129 {
130 return 0;
131 }
132
133 /// <summary>
134 /// 加中毒值
135 /// </summary>
136 /// <param name="percent">百分比</param>
137 /// <param name="speed">下降速度(刻度/秒)</param>
138 public void AddPoison(float percent, float speed)
139 {
140 }
141
142 /// <summary>
143 /// 加护盾值
144 /// </summary>
145 /// <param name="percent">百分比</param>
146 /// <param name="time">持续时间(秒)</param>
147 public void AddShield(float percent, float time)
148 {
149 }
150
151 #endregion
152 }
View Code
接下来我们处理具体的逻辑。
1. 自动隐藏:
自动隐藏的需求是,在5秒内未产生任何形式的血量变化,则淡出隐藏。一旦产生血量变化,淡入显示。
淡入淡出是需要Alpha值来控制的。我们直接改变_barSprite这个字段(即最上层的Healthbar上的UISprite)的alpha值,则其子物体会一起产生Alpha值变化的效果。
修改IsVisible属性:
1 private bool IsVisible
2 {
3 get { return _barSprite.color.a >= 1; }
4 set
5 {
6 _timer = 0;
7 if (value != IsVisible && !_isFading)
8 {
9 _isFading = true;
10 if (value)
11 {
12 iTween.ValueTo(gameObject,
13 iTween.Hash("from", 0, "to", 1, "time", AnimDuration, "easetype",
14 EaseType, "onupdate", "OnFadeIn",
15 "onupdatetarget", gameObject, "oncomplete", "OnFadeInComplete", "oncompletetarget", gameObject));
16 }
17 else
18 {
19 iTween.ValueTo(gameObject,
20 iTween.Hash("from", 1, "to", 0, "time", AnimDuration, "easetype",
21 EaseType, "onupdate", "OnFadeOut",
22 "onupdatetarget", gameObject, "oncomplete", "OnFadeOutComplete", "oncompletetarget", gameObject));
23 }
24 }
25 }
26 }
再添加iTween中引用的四个方法:
1 private void OnFadeIn(float value)
2 {
3 _barSprite.color = new Color(1, 1, 1, value);
4 }
5
6 private void OnFadeInComplete()
7 {
8 _isFading = false;
9 _timer = 0;
10 }
11
12 private void OnFadeOut(float value)
13 {
14 _barSprite.color = new Color(1, 1, 1, value);
15 }
16
17 private void OnFadeOutComplete()
18 {
19 _isFading = false;
20 _timer = 0;
21 }
现在,直接改变IsVisible即可控制淡入淡出。我们还需要在Update里检查血量变化和设置隐藏,即修改IsVisible:
1 private void Update()
2 {
3 if (autoHide && !_isFading && IsVisible && !IsPoisoned)
4 {
5 _timer += Time.deltaTime;
6 if (_timer > 5f)
7 {
8 IsVisible = false;
9 }
10 }
11 }
上面设置的阀值为5秒。实际上这个5该提取出来做属性或字段。在收到伤害,护盾等情况时,我们需要手动改变IsVisible。
2. 减血:
1 /// <summary>
2 /// 加伤害
3 /// </summary>
4 /// <param name="percent">将造成的伤害百分比(小于1)</param>
5 /// <returns>剩余血量百分比</returns>
6 public float AddDamage(float percent)
7 {
8 if (!IsVisible)
9 {
10 IsVisible = true;
11 }
12 if (percent > 1f)
13 {
14 Debug.LogWarning(string.Format("Illegal damage percent: -{0}", percent));
15 return _normal.value;
16 }
17 if (_normal.value <= 0f)
18 {
19 Debug.LogWarning(string.Format("Health is already below zero: -{0}", percent));
20 return _normal.value;
21 }
22 if (IsShielded)
23 {
24 _shieldedDmg.value = _shielded.value;
25 _shielded.value -= percent;
26 _shieldedDmg.gameObject.SetActive(true);
27 iTween.ValueTo(gameObject,
28 iTween.Hash("from", _shieldedDmg.value, "to", _shielded.value, "time", AnimDuration, "easetype",
29 EaseType, "onupdate", "OnShieldedDamage",
30 "onupdatetarget", gameObject, "oncomplete", "ShieldedDamageDone", "oncompletetarget", gameObject));
31 //if damage lows the shield value to zero, take health instead
32 //...
33 }
34 else
35 {
36 _normalDmg.value = _normal.value;
37 _normal.value -= percent;
38 _normalDmg.gameObject.SetActive(true);
39 iTween.ValueTo(gameObject,
40 iTween.Hash("from", _normalDmg.value, "to", _normal.value, "time", AnimDuration, "easetype", EaseType,
41 "onupdate", "OnNormalDamage",
42 "onupdatetarget", gameObject, "oncomplete", "NormalDamageDone", "oncompletetarget", gameObject));
43 }
44
45 return _normal.value;
46 }
上面有判断护盾。当受到的伤害不大于护盾值时,只会减少护盾,若未大于护盾值,则会从正常血量里扣除剩余的值(此处我未处理这种情况。只标了注释,算是留给大家的一个题目...)。
注意,血量值得变化是按百分比来算的。所以各种参数应该在折算后传入。比如你满血为100,受到20点伤害,那么应该AddDamage(0.2f);
添加iTween里的引用:
1 private void OnNormalDamage(float value)
2 {
3 _normalDmg.value = value;
4 }
5
6 private void NormalDamageDone()
7 {
8 _normalDmg.gameObject.SetActive(false);
9 }
10
11 private void OnShieldedDamage(float value)
12 {
13 _shieldedDmg.value = value;
14 }
15
16 private void ShieldedDamageDone()
17 {
18 _shieldedDmg.gameObject.SetActive(false);
19 }
注意在变化完成,及时隐藏相关底色。
3. 护盾:
1 /// <summary>
2 /// 加护盾值
3 /// </summary>
4 /// <param name="percent">百分比</param>
5 /// <param name="time">持续时间(秒)</param>
6 public void AddShield(float percent, float time)
7 {
8 //若将增加的护盾值使总值超过100%
9 if (_normal.value + percent + _shieldMod > 1f)
10 {
11 percent = _normal.value + percent + _shieldMod - 1;
12 _normal.value -= percent;
13 _shieldMod += percent;
14 }
15 //若已中毒
16 if (IsPoisoned)
17 {
18 //若将要增加的护盾值大于中毒(剩余)值
19 if (percent > _poisoned.value)
20 {
21 percent -= _poisoned.value;
22 }
23 else //否则
24 {
25 percent = percent - _poisoned.value;
26 }
27 }
28 _shielded.value = _normal.value + percent;
29 IsShielded = true;
30 iTween.ValueTo(gameObject,
31 iTween.Hash("from", 0, "to", time, "time", time, "onupdate", "OnShield", "oncomplete", "ShieldTimeOut",
32 "oncompleteparams", _shieldMod,
33 "oncompletetarget", gameObject));
34 }
上面出现里一个float型的_shieldMod之前并没有声明。事实上我后来了解到需求里护盾是不能叠加的,后加的护盾只会覆盖之前的护盾。所以这个用来存储多重护盾值得_shieldMod就没用了。移除即可。记得要从iTween.ValueTo里也将其移除并修改对应方法签名(ShieldTimeOut)。
1 private void OnShield(float value)
2 {
3 }
4
5 private void ShieldTimeOut(float modPercent)
6 {
7 _shieldMod = Mathf.Max(0, _shieldMod - modPercent);
8 _normal.value += modPercent;
9 IsShielded = false;
10 _shielded.value = _normal.value;
11 _shieldedDmg.value = _normal.value;
12 }
护盾到期是在ShieldTimeOut里处理的。此处并没有渐变消失,而是啪一下没了=。=
4.中毒:
1 /// <summary>
2 /// 加中毒值
3 /// </summary>
4 /// <param name="percent">百分比</param>
5 /// <param name="speed">下降速度(刻度/秒)</param>
6 public void AddPoison(float percent, float speed)
7 {
8 //若已加护盾
9 if (IsShielded)
10 {
11 //若将要增加的中毒值大于护盾值
12 if (percent > _shielded.value)
13 {
14 percent -= _shielded.value;
15 }
16 else //否则
17 {
18 percent = _shielded.value - percent;
19 }
20 }
21 if (percent < 0)
22 {
23 return;
24 }
25 _normal.value -= percent;
26 IsPoisoned = true;
27 iTween.ValueTo(gameObject,
28 iTween.Hash("from", percent, "to", 0, "speed", speed, "easetype", EaseType, "onupdate", "OnPoison", "onupdatetarget",
29 gameObject,
30 "oncomplete",
31 "PoisonTimeOut",
32 "oncompletetarget", gameObject));
33 }
1 private void OnPoison(float value)
2 {
3 _poisoned.value = _normal.value + value;
4 }
5
6 private void PoisonTimeOut()
7 {
8 IsPoisoned = false;
9 _poisoned.value = _normal.value;
10 }
中毒的递减效果在OnPoison里实现。
最后添加测试代码:
1 /// <summary>
2 /// Debug Testing
3 /// </summary>
4 private void OnGUI()
5 {
6 if (GUI.Button(new Rect(10, 10, 200, 100), "Hit - 20%"))
7 {
8 AddDamage(0.2f);
9 }
10 if (GUI.Button(new Rect(10, 120, 200, 100), "Shield + 30%(3s)"))
11 {
12 AddShield(0.3f, 3);
13 }
14 if (GUI.Button(new Rect(10, 230, 200, 100), "Poison + 10%(5%/s)"))
15 {
16 AddPoison(0.1f, 0.05f);
17 }
18 }
到此就差不多完成了。拖出来当预置,就成了一个自定义控件。
事实上还存在许多bug。尤其是在面临【又加护盾又中毒又受伤害】这类情况下。我没有去处理这样的逻辑因为项目里不需要。这里只是提供一个思路给大家。如果能抛砖引玉当然最好了~
另外,里面的iTween这样用起来会很麻烦。我会另写一篇,介绍我的iTween自定义扩展。