学习目标:
上期我们学习怎么进行后处理渲染以及制作玩家子弹,但我们添加了碰撞体,为子弹设置了伤害,但却没有制作我们的血条和能量条以及属于玩家和敌人的生命值系统和能量值系统。这一期就讲一下怎么实现这两部分功能
制作生命值系统:
首先我们写个Character总类让Player和Enemy继承它。
先简单写写Character受到伤害的TakeDmaage和死亡Die和一些属性,以及一些协程包括在没受到伤害的时候持续回血
using System.Collections;
using UnityEngine;
public class Character : MonoBehaviour
{
[Header("--- HEALTH ---")]
[SerializeField] protected float maxHealth;
protected float health;
[SerializeField] StateBar onHeadHealthBar;
[SerializeField] bool showOnHealthBar = true;
protected virtual void OnEnable()
{
health = maxHealth;
if (showOnHealthBar)
{
ShowOnHealthBar();
}
else
{
HideOnHealthBar();
}
}
public void ShowOnHealthBar()
{
onHeadHealthBar.gameObject.SetActive(true);
onHeadHealthBar.Initialize(health, maxHealth);
}
public void HideOnHealthBar()
{
onHeadHealthBar.gameObject.SetActive(false);
}
public virtual void TakeDamage(float damage)
{
health -= damage;
if (showOnHealthBar && gameObject.activeSelf)
{
onHeadHealthBar.UpdateStates(health, maxHealth);
}
if(health <= 0)
{
Die();
}
}
public virtual void Die()
{
health = 0;
gameObject.SetActive(false);
}
public virtual void RestoreHealth(float value)
{
if (health == maxHealth)
return;
health = Mathf.Clamp(health + value, 0, maxHealth);
if (showOnHealthBar)
{
onHeadHealthBar.UpdateStates(health, maxHealth);
}
}
protected IEnumerator HealthPercentageCoroutine(WaitForSeconds waitTime,float percent)
{
while(health < maxHealth)
{
yield return waitTime;
RestoreHealth(percent * maxHealth);
}
}
protected IEnumerator DamageOverTime(WaitForSeconds waitTime, float percent)
{
while (health > 0)
{
yield return waitTime;
RestoreHealth(percent * maxHealth);
}
}
}
接着我们让Player和ENEMY脚本继承它
public class Player : Character
{
[SerializeField] StateBar_HUD stateBar_HUD;
[SerializeField] bool genearatedHealth = true;
[SerializeField] float healthGenerateTime;
[SerializeField,Range(0f,1f)] float healthRegeneatePercent;
[Header("--- COROUTINE ---")]
Coroutine moveCoroutine;
Coroutine healthRegenerateCoroutine;
WaitForSeconds waitForFireInterval;
WaitForSeconds waitHealthGeneratedTime;
}
void Start()
{
rigi2D.gravityScale = 0f;
waitForFireInterval = new WaitForSeconds(fireInterval);
waitHealthGeneratedTime = new WaitForSeconds(healthGenerateTime);
stateBar_HUD.Initialize(health, maxHealth);
input.EnableGamePlay(); //激活动作表
}
public override void TakeDamage(float damage)
{
base.TakeDamage(damage);
stateBar_HUD.UpdateStates(health, maxHealth);
if (gameObject.activeSelf)
{
if(healthRegenerateCoroutine != null)
{
StopCoroutine(healthRegenerateCoroutine);
}
healthRegenerateCoroutine = StartCoroutine(HealthPercentageCoroutine(waitHealthGeneratedTime, healthRegeneatePercent));
}
}
public override void RestoreHealth(float value)
{
base.RestoreHealth(value);
stateBar_HUD.UpdateStates(health, maxHealth);
}
public override void Die()
{
stateBar_HUD.UpdateStates(0f, maxHealth);
base.Die();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : Character
{
[SerializeField] int deathEnergyBouns = 3;
public override void Die()
{
base.Die();
}
}
因此我们可以在Projectile的OnCollisionEnter2D来让继承Character的脚本受到伤害
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Projectile : MonoBehaviour
{
[SerializeField] float moveSpeed = 10f;
[SerializeField] protected Vector2 moveDirection;
[SerializeField] float damage;
[SerializeField] AudioData[] hitSFX;
[SerializeField] GameObject hitVFX;
protected GameObject target;
protected virtual void OnEnable()
{
StartCoroutine(MoveDirectly());
}
IEnumerator MoveDirectly()
{
while (gameObject.activeSelf)
{
transform.Translate(moveDirection * moveSpeed * Time.deltaTime);
yield return null;
}
}
protected virtual void OnCollisionEnter2D(Collision2D collision)
{
if(collision.gameObject.TryGetComponent<Character>(out Character character)) //判断碰撞的游戏对象是否为Character类,所消耗的性能更少
{
character.TakeDamage(damage);
var contactPoint = collision.GetContact(0); //得到collision的第一个接触点
PoolManager.Release(hitVFX, contactPoint.point, Quaternion.LookRotation(contactPoint.normal)); //contactPoint.normal法线方向,某个顶点在3D空间中的朝向
gameObject.SetActive(false);
}
}
}
这样我们可以为Enemy和Player设置生命值了。
制作能量值:
我们先创建一个PlayerEnergy挂载给Player。
因为能量条是不同场景中不会销毁的,因此使用单例模式。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : Component //约定泛型类型必须是Component
{
public static T Instance { get; private set; } //声明一个只读类型的单例
protected virtual void Awake()
{
Instance = this as T;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerEnergy : Singleton<PlayerEnergy>
{
[SerializeField] EnergyBar energyBar;
public const int MAX = 100;
public const int PERCENT = 1;
int energy;
void Start()
{
energyBar.Initialize(energy, MAX);
Obtain(75);
}
public void Obtain(int value) //获得能量
{
if (energy == MAX) return;
energy = Mathf.Clamp(energy + value, 0, MAX);
energyBar.UpdateStates(energy, MAX); //更新能量条
}
public void Use(int value)
{
energy -= value;
energyBar.UpdateStates(energy, MAX);
}
public bool IsEnough(int value) => energy >= value; //判断能量是否能使用
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerProjectile : Projectile
{
TrailRenderer trail;
private void Awake()
{
trail = GetComponentInChildren<TrailRenderer>();
if (moveDirection != Vector2.right)
{
transform.GetChild(0).rotation = Quaternion.FromToRotation(Vector2.right, moveDirection);
}
}
private void OnDisable()
{
trail.Clear();
}
protected override void OnCollisionEnter2D(Collision2D collision)
{
base.OnCollisionEnter2D(collision);
PlayerEnergy.Instance.Obtain(PlayerEnergy.PERCENT);
}
}
首先要在攻击到敌人的时候增加能量。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : Character
{
[SerializeField] int deathEnergyBouns = 3;
public override void Die()
{
PlayerEnergy.Instance.Obtain(deathEnergyBouns);
EnemyManager.Instance.RemoveList(gameObject);
base.Die();
}
}
制作生命条和能量条:
先创建一个UI作为所以ui的父对象,然后将创建一个HUD Player Health Bar的Canvas并作为子对象然后设置好它的参数。
在进行接下来的操作之前,让我们创建好他们的Material材质。
接着我们添加一个Image背景,依次添加以下组件。
EnergyBar也类似可以直接复制粘贴HealthBar,只需要稍微更改一下它们的位置即可
为了管理这些状态能量条,我们同样也需要一个总类StateBar
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class StateBar : MonoBehaviour
{
[SerializeField] Image fillImageBack;
[SerializeField] Image fillImageFront;
[SerializeField] float fillSpeed = 0.1f;
[SerializeField] bool delayFill = true;
[SerializeField] float fillDelay = 0.5f;
Canvas canvas;
float previousFillAmount;
float currentFillAmount;
protected float targetFillAmount;
float t;
Coroutine bufferdFillingCoroutine;
WaitForSeconds waitForDelayFill;
private void Awake()
{
canvas = GetComponent<Canvas>();
canvas.worldCamera = Camera.main;
waitForDelayFill = new WaitForSeconds(fillDelay);
}
private void OnDisable()
{
StopAllCoroutines();
}
public virtual void Initialize(float currentValue,float maxValue)
{
currentFillAmount = currentValue / maxValue;
targetFillAmount = currentFillAmount;
fillImageBack.fillAmount = currentFillAmount;
fillImageFront.fillAmount = currentFillAmount;
}
public void UpdateStates(float currentValue,float maxValue)
{
targetFillAmount = currentValue / maxValue;
if(bufferdFillingCoroutine != null)
{
StopCoroutine(bufferdFillingCoroutine);
}
//如果状态值在减少的时候
//让front的图片立即减少到目标值
//back的图片则根据fillSpeed慢慢减少到目标值
if(currentFillAmount > targetFillAmount)
{
fillImageFront.fillAmount = targetFillAmount;
bufferdFillingCoroutine = StartCoroutine(BufferedFillingCoroutine(fillImageBack));
return;
}
//如果状态值在增加的时候
//让back的图片立即减少到目标值
//front的图片则根据fillSpeed慢慢减少到目标值
if (currentFillAmount < targetFillAmount)
{
fillImageBack.fillAmount = targetFillAmount;
bufferdFillingCoroutine = StartCoroutine(BufferedFillingCoroutine(fillImageFront));
}
}
protected virtual IEnumerator BufferedFillingCoroutine(Image image)
{
if (delayFill)
{
yield return waitForDelayFill;
}
t = 0f;
previousFillAmount = currentFillAmount;
while (t < 1f)
{
t += Time.deltaTime * fillSpeed;
currentFillAmount = Mathf.Lerp(previousFillAmount, targetFillAmount, t);
image.fillAmount = currentFillAmount;
yield return null;
}
}
}
为HealthBar的Canvas添加一个脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class StateBar_HUD : StateBar
{
[SerializeField] Text percentText;
void SetPercent()
{
percentText.text = Mathf.RoundToInt(targetFillAmount * 100f).ToString();
}
public override void Initialize(float currentValue, float maxValue)
{
base.Initialize(currentValue, maxValue);
SetPercent();
}
protected override IEnumerator BufferedFillingCoroutine(Image image)
{
SetPercent();
return base.BufferedFillingCoroutine(image);
}
}
再为我们的能量条
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnergyBar : StateBar_HUD
{
}
最后我们再制作一个玩家头顶的血量条。
为什么我们频繁使用前后两张照片呢?因为我们脚本设计的时候使用的让收到伤害的时候血条不是立刻减少到相应的值,而是调用协程让后面的图片先减少到相应的值,再将前面的血条缓慢减少到相应的值。
注意!它们前后两张的图片的材质并不相同。
学习产出:
玩家和敌人都正常掉血了。