這陣子項目中需要用到一種特殊樣式的血條。描述如下:
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 }
接下來我們處理具體的邏輯。
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自定義擴展。
源碼請見我的github。
https://github.com/theoxuan/GeneralGame/blob/master/Assets/Resources/Healthbar.prefab
https://github.com/theoxuan/GeneralGame/blob/master/Assets/Script/UIHealthbar.cs
