UGUI study notes (12) self-made blood bar control

1. Effect display

2. Implementation process

2.1 Preparations

First use "Image" in the scene to create the following structure and name it "LifeBar". It should be noted that the internal "Image" needs to set the anchor point to the leftmost and the height to be adaptive. Mount the script with the same name on the parent element and make "LifeBar" as a prefab.

The reason why two blood bars "OuterBar" and "InnerBar" were created is to create the effect of multi-layer blood bars.

Then create an enemy randomly in the scene (requires "MeshRenderer"), and then mount a control script. My script here is named "KeLiController"

2.2 Dynamically create blood bars

Next, the blood bar needs to be dynamically created when the game is running. Add the following code in "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);
}

The created code then needs to be moved on top of the enemy. We know that the position of the model is the position of the center point of the foot, but how to get the height of the model? In fact, there are related properties in the "Mesh Renderer" component mounted on the model. We can get the Y value of the highest point of "Mesh Renderer" as the offset when "LifeBar" is initialized, and then update the position of the blood bar continuously in Update (don't forget to convert the world coordinates to screen coordinates)

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);
    }
}

Run the game and you can see that the health bar has been correctly generated on top of the enemy's head. Just add some movement control logic to the enemy, and you can find that the blood bar will also move with the character.

2.3 Initialize the blood bar

After the blood bar is created, it needs to be initialized, including setting the color and picture of the blood bar. Create a new "LifeBarData" class to encapsulate the data of the blood bar.

public class LifeBarData
{
    
    
    public Sprite BarSprite;
    public Color BarColor;

    public LifeBarData(Sprite barSprite, Color barColor)
    {
    
    
        BarSprite = barSprite;
        BarColor = barColor;
    }
}

Create a "LifeBarItem" class to control a single health bar. "InnerBar", "OuterBar" and the "AdditionBar" below them all need to mount this script. Define the necessary fields in the "LifeBarItem" class and expose the initialization 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);
        }
    }
}

The reason why "AdditionBar" is set to be the same as the parent element here is to create a transition effect when blood is deducted. It will be explained later.

Next, hold "OuterBar" and "InnerBar" in "LifeBar" and initialize them. Alternatively, the data required for initialization can be passed in via a collection. How many pieces of data there are in the collection means how many bloodlines there are. Then pass in an integer parameter to indicate the total blood volume, and then you can calculate how much width the unit blood volume occupies.

// 血条数据
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]);
	}
}

Finally, pass in a set of test data in "KeLiController", and run it to see the effect

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 Blood deduction logic

Write the blood deduction logic below. First of all, for "AdditionBar", you can directly use DOTween to make a fading animation. For the outer health bar, a width change value can be passed in. Adjust the width of the health bar according to this change value. However, it should be considered that if the blood addition/deduction exceeds the range of the current blood bar, the excess value needs to be returned in order to process the blood deduction logic of the subsequent blood bar.

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;
}

After receiving the returned excess value in "LifeBar", it is necessary to reset the level and width of the blood bar according to the situation

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);
}

At this point, the main logic of adding blood/deducting blood to the blood bar is completed. Add two click events on the enemy's script, the left button is for deducting blood, and the right button is for adding blood. Run the game to see the effect

if (Input.GetMouseButtonDown(0))  
{
    
      
    _lifeBar.ChangeLife(-70);  
}else if (Input.GetMouseButtonDown(1))  
{
    
      
    _lifeBar.ChangeLife(70);  
}


Source code download

Guess you like

Origin blog.csdn.net/LWR_Shadow/article/details/126833809