• 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    然后需要将创建出来的代码移动到敌人头顶上。我们知道模型的位置就是脚下中心点的位置,但是如何获取到模型的高度呢?其实模型上挂载的「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);
        }
    }
    
    
    • 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

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

    2.3 初始化血条

    血条创建出来后还需要对其进行初始化,包括设置血条的颜色、图片等。新建一个「LifeBarData」类用来对血条的数据进行封装。

    public class LifeBarData
    {
        public Sprite BarSprite;
        public Color BarColor;
    
        public LifeBarData(Sprite barSprite, Color barColor)
        {
            BarSprite = barSprite;
            BarColor = barColor;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    再创建一个「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);
            }
        }
    }
    
    • 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

    这里「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]);
    	}
    }
    
    • 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

    最后在「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);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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;  
    }
    ///   
    /// 血量改变事件  
    ///   
    /// 改变量(宽度)  
    /// 
    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;
    }
    
    • 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

    在「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);
    }
    
    • 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

    至此,血条加血/扣血的主要逻辑就算完成了。在敌人的脚本上添加两个点击事件,左键为扣血,右键为加血。运行游戏看下效果

    if (Input.GetMouseButtonDown(0))  
    {  
        _lifeBar.ChangeLife(-70);  
    }else if (Input.GetMouseButtonDown(1))  
    {  
        _lifeBar.ChangeLife(70);  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7


    源码下载

  • 相关阅读:
    【网络安全】黑客自学笔记
    CentOS7 firewall使用(开放和禁止端口、端口转发)
    定制化、高可用前台样式处理方案——tailwindcss
    需求工程咨询和实施服务
    Spring注解详解
    二氧化钛纳米粒TIO2修饰多肽R8/CTT2/CCK8/GE11/cTAT/CPP/RVG29/SP94(无机纳米粒子偶联多肽)
    【正点原子I.MX6U-MINI应用篇】1、编写第一个应用App程序helloworld
    【PAT甲级】1136 A Delayed Palindrome
    使用HTML制作静态宠物网站——蓝色版爱宠之家(HTML+CSS)
    再看const成员函数
  • 原文地址:https://blog.csdn.net/LWR_Shadow/article/details/126833809