UGUI学习笔记(十二)自制血条控件

一、效果展示

二、实现过程

2.1 准备工作

首先在场景中使用「Image」创建如下结构并命名为「LifeBar」。需要注意的是内部的「Image」都需要将锚点设置到最左侧,高度设置为自适应。在父元素上挂载同名脚本,将「LifeBar」制作为预制体。

之所以创建了「OuterBar」和「InnerBar」两个血条,是为了做出多层血条的效果。

然后在场景中随便创建一个敌人(需要有「MeshRenderer」),然后挂载一个控制脚本。我这里的脚本命名为「KeLiController」

2.2 动态创建血条

接下来需要在游戏运行时,将血条动态创建出来。在「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);
    }

    /// <summary>
    /// 获取Renderer最高点的Y值
    /// </summary>
    /// <param name="target"></param>
    /// <returns></returns>
    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);
    }
}

运行游戏可以看到,血条已经正确生成在了敌人头顶上。随便给敌人加点移动的控制逻辑,可以发现血条也会跟随角色移动。

2.3 初始化血条

血条创建出来后还需要对其进行初始化,包括设置血条的颜色、图片等。新建一个「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;

/// <summary>
/// 初始化
/// </summary>
/// <param name="target">目标物体</param>
/// <param name="lifeMax">最大血量</param>
/// <param name="data">血条数据</param>
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);
}
/// <summary>
/// 设置内外血条数据
/// </summary>
/// <param name="index"></param>
/// <param name="data"></param>
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);

2.4 扣血逻辑

下面编写扣血逻辑。首先对于「AdditionBar」来说,可以直接使用DOTween做一个渐隐动画。对于外层血条,可以传入一个宽度的改变值。根据这个改变值调整血条的宽度。不过要考虑到如果加血/扣血超出了当前血条的范围,就需要把超出的值返回出去,以便处理后续血条的扣血逻辑。

private float _defaultWidth;  
public void Init()  
{
    
      
    var additionBar = transform.Find("AdditionBar");  
    if (additionBar != null)  
        _child = additionBar.AddComponent<LifeBarItem>();  
    _defaultWidth = Rect.rect.width;  
}
/// <summary>  
/// 血量改变事件  
/// </summary>  
/// <param name="changeValue">改变量(宽度)</param>  
/// <returns></returns>
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();
}
/// <summary>  
/// 获取超出部分的宽度  
/// </summary>  
/// <returns></returns>
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);
	}
}
/// <summary>
/// 更改下标
/// </summary>
/// <param name="isAdd">是否是加血</param>
/// <returns></returns>
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);  
}


源码下载

猜你喜欢

转载自blog.csdn.net/LWR_Shadow/article/details/126833809