首先在场景中使用「Image」创建如下结构并命名为「LifeBar」。需要注意的是内部的「Image」都需要将锚点设置到最左侧,高度设置为自适应。在父元素上挂载同名脚本,将「LifeBar」制作为预制体。
之所以创建了「OuterBar」和「InnerBar」两个血条,是为了做出多层血条的效果。
然后在场景中随便创建一个敌人(需要有「MeshRenderer」),然后挂载一个控制脚本。我这里的脚本命名为「KeLiController」
接下来需要在游戏运行时,将血条动态创建出来。在「KeLiController」中添加如下代码
private LifeBar _lifeBar;
void Start()
{
Canvas canvas = FindObjectOfType<Canvas>();
if (canvas == null)
{
Debug.LogError("场景中没有Canvas控件");
return;
}
SpawnLifeBar(canvas);
}
private void SpawnLifeBar(Canvas canvas)
{
GameObject lifeBar = Resources.Load<GameObject>("LifeBar");
_lifeBar = Instantiate(lifeBar, canvas.transform).AddComponent<LifeBar>();
// 初始化操作
_lifeBar.Init(transform);
}
然后需要将创建出来的代码移动到敌人头顶上。我们知道模型的位置就是脚下中心点的位置,但是如何获取到模型的高度呢?其实模型上挂载的「Mesh Renderer」组件中就有相关的属性。我们可以在「LifeBar」进行初始化时获取到「Mesh Renderer」最高点的Y值作为偏移量,然后在Update中不断更新血条的位置(别忘了把世界坐标转换为屏幕坐标)
public class LifeBar : MonoBehaviour
{
private Transform _target;
private float _offsetY;
public void Init(Transform target)
{
_target = target;
_offsetY = GetOffsetY(target);
}
///
/// 获取Renderer最高点的Y值
///
///
///
private float GetOffsetY(Transform target)
{
Renderer ren = target.GetComponentInChildren<Renderer>();
if (ren == null)
return 0;
return ren.bounds.max.y;
}
private void Update()
{
if(_target == null)
return;
transform.position = Camera.main.WorldToScreenPoint(_target.position + Vector3.up*_offsetY);
}
}
运行游戏可以看到,血条已经正确生成在了敌人头顶上。随便给敌人加点移动的控制逻辑,可以发现血条也会跟随角色移动。
血条创建出来后还需要对其进行初始化,包括设置血条的颜色、图片等。新建一个「LifeBarData」类用来对血条的数据进行封装。
public class LifeBarData
{
public Sprite BarSprite;
public Color BarColor;
public LifeBarData(Sprite barSprite, Color barColor)
{
BarSprite = barSprite;
BarColor = barColor;
}
}
再创建一个「LifeBarItem」类用来控制单个的血条。「InnerBar」、「OuterBar」和它们下面的「AdditionBar」都需要挂载这个脚本。在「LifeBarItem」类中定义出必要的字段并暴露出初始化API
public class LifeBarItem : MonoBehaviour
{
private Image _img;
private Image Img
{
get
{
if (_img == null)
_img = GetComponent<Image>();
return _img;
}
}
private RectTransform _rect;
private RectTransform Rect
{
get
{
if (_rect == null)
_rect = GetComponent<RectTransform>();
return _rect;
}
}
private LifeBarItem _child;
public void Init()
{
var additionBar = transform.Find("AdditionBar");
if (additionBar != null)
_child = additionBar.AddComponent<LifeBarItem>();
}
public void SetData(LifeBarData data)
{
Img.color = data.BarColor;
if (data.BarSprite != null)
{
Img.sprite = data.BarSprite;
}
if (_child != null)
{
_child.SetData(data);
}
}
}
这里「AdditionBar」之所以设置成与父元素相同,是为了制作扣血时的过渡特效。后面会进行说明。
接下来在「LifeBar」中持有「OuterBar」和「InnerBar」并对其进行初始化设置即可。另外,初始化所需的数据可以通过一个集合传入。集合有多少条数据,就代表有多少行血条。再传入一个整型参数表示总血量,就可以计算出单位血量占多少宽度。
// 血条数据
private List<LifeBarData> _data;
// 外层血条
private LifeBarItem _outerBar;
// 内层血条
private LifeBarItem _innerBar;
// 单位血量所占宽度
private float _unitLifeScale;
// 当前血条下标
private int _index;
///
/// 初始化
///
/// 目标物体
/// 最大血量
/// 血条数据
public void Init(Transform target,int lifeMax,List<LifeBarData> data)
{
_target = target;
_offsetY = GetOffsetY(target);
_data = data;
_outerBar = transform.Find("OuterBar").AddComponent<LifeBarItem>();
_innerBar = transform.Find("InnerBar").AddComponent<LifeBarItem>();
_outerBar.Init();
_innerBar.Init();
_unitLifeScale = GetComponent<RectTransform>().rect.width * data.Count / lifeMax;
SetBarData(_index, data);
}
///
/// 设置内外血条数据
///
///
///
private void SetBarData(int index, List<LifeBarData> data)
{
if(index < 0 || index >= data.Count)
return;
_outerBar.SetData(data[index]);
if (index + 1 >= data.Count)
{
_innerBar.SetData(new LifeBarData(null,Color.white));
}
else
{
_innerBar.SetData(data[index+1]);
}
}
最后在「KeLiController」中传入一组测试数据,运行看下效果
List<LifeBarData> data = new();
data.Add(new LifeBarData(null,Color.blue));
data.Add(new LifeBarData(null,Color.green));
data.Add(new LifeBarData(null,Color.yellow));
_lifeBar.Init(transform,350,data);
下面编写扣血逻辑。首先对于「AdditionBar」来说,可以直接使用DOTween做一个渐隐动画。对于外层血条,可以传入一个宽度的改变值。根据这个改变值调整血条的宽度。不过要考虑到如果加血/扣血超出了当前血条的范围,就需要把超出的值返回出去,以便处理后续血条的扣血逻辑。
private float _defaultWidth;
public void Init()
{
var additionBar = transform.Find("AdditionBar");
if (additionBar != null)
_child = additionBar.AddComponent<LifeBarItem>();
_defaultWidth = Rect.rect.width;
}
///
/// 血量改变事件
///
/// 改变量(宽度)
///
public float ChangeLife(float changeValue)
{
if (_child != null)
{
// 清除未播放完的动画
_child.DOKill();
_child.Img.color = Img.color;
_child.Rect.sizeDelta = Rect.sizeDelta;
_child.Img.DOFade(0, 0.5f);
}
Rect.sizeDelta += changeValue * Vector2.right;
return GetOutRange();
}
///
/// 获取超出部分的宽度
///
///
private float GetOutRange()
{
float offset = 0;
var rectWidth = Rect.rect.width;
if (rectWidth < 0)
{
offset = rectWidth;
ResetToZero();
}
else if (rectWidth > _defaultWidth)
{
offset = rectWidth - _defaultWidth;
ResetToDefault();
}
return offset;
}
public void ResetToZero()
{
Rect.sizeDelta = Vector2.zero;
}
public void ResetToDefault()
{
Rect.sizeDelta = _defaultWidth * Vector2.right;
}
在「LifeBar」中接收到返回的超出值后,需要分情况对血条的层级、宽度等进行重新设置
public void ChangeLife(float changeValue)
{
var extraWidth = _outerBar.ChangeLife(changeValue * _unitLifeScale);
// 当前血条不够扣
if (extraWidth < 0 && ChangeIndex(false))
{
// 交换前后血条的指针
ExChangeBar();
// 设置层级,使其显示在前面
_outerBar.transform.SetAsLastSibling();
// 内层血条恢复成默认大小
_innerBar.ResetToDefault();
SetBarData(_index,_data);
ChangeLife(extraWidth/_unitLifeScale);
}
// 当前血条不够加
else if (extraWidth > 0 && ChangeIndex(true))
{
// 交换前后血条的指针
ExChangeBar();
// 设置层级,使其显示在前面
_outerBar.transform.SetAsLastSibling();
// 外层血条设置为0
_outerBar.ResetToZero();
SetBarData(_index,_data);
ChangeLife(extraWidth/_unitLifeScale);
}
}
///
/// 更改下标
///
/// 是否是加血
///
private bool ChangeIndex(bool isAdd)
{
// 加血往前移,扣血往后移
int newIndex = _index + (isAdd ? -1 : 1);
if (newIndex >= 0 && newIndex < _data.Count)
{
_index = newIndex;
return true;
}
return false;
}
private void ExChangeBar()
{
(_outerBar, _innerBar) = (_innerBar, _outerBar);
}
至此,血条加血/扣血的主要逻辑就算完成了。在敌人的脚本上添加两个点击事件,左键为扣血,右键为加血。运行游戏看下效果
if (Input.GetMouseButtonDown(0))
{
_lifeBar.ChangeLife(-70);
}else if (Input.GetMouseButtonDown(1))
{
_lifeBar.ChangeLife(70);
}