게임에서 데미지 인디케이터는 플레이어가 전투 상황을 더 잘 이해하고 적에 대한 전략을 수립할 수 있도록 돕습니다. 이는 플레이어의 강력함과 적의 상황을 더 잘 파악하며, 자신의 능력치와 전략을 최적화할 수 있도록 돕습니다.

 

📺 미리보기

 

💬 서론

  • 본 글에서 다루는 기능은 오브젝트 풀링을 사용합니다.
  • 오브젝트 풀링을 사용하여 무거운 인스턴싱을 최소화하고 사용이 완료된 대상을 재사용합니다.
  • 오브젝트 풀링에 대한 내용은 "[유니티] 오브젝트 풀 (Object Pool)"에서 확인할 수 있습니다.

 

📖 구현 내용

  • 플레이어가 입힌 피해량 중 실제로 적에게 들어간 피해량을 시각화하여 보여줍니다.
  • 3D 월드에 배치되어 나타나며, Overlay를 사용하여 다른 오브젝트에 가려지지 않습니다.
  • 표시되는 색상을 쉽게 바꿀 수 있어 피해량 뿐만 아니라 체력 및 마나 회복, 마나소모 등 여러가지 기능도 함께 사용할 수 있습니다.
  • 오브젝트 풀링을 사용하는 모델입니다.
  • 데미지 라벨이 항상 카메라를 바라봅니다.

 

⚒️ 구현

· DamageIndicator.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

[RequireComponent(typeof(Rigidbody))]
public class DamageIndicator : MonoBehaviour
{
    [Header("프리팹의 리지드바디")]
    [SerializeField] Rigidbody mRigidBody;

    [Header("프리팹의 데미지 라벨")]
    [SerializeField] TextMeshPro mDamageLabel;

    [Header("프리팹의 자식 객체 (Look Camera)")]
    [SerializeField] Transform mChildTransform;

    private Coroutine? mCoFadeLabel;

    private void Update()
    {
        mChildTransform.LookAt(Camera.main.transform);
    }

    public void Init(Vector3 pos, float amount, Color color, float size)
    {
        // 만약 호출된 값이 0이라면 리턴
        if(Mathf.RoundToInt(amount) == 0)
        {
            gameObject.SetActive(false);
            return;
        }

        // 재사용시 기존의 상태를 초기화
        mRigidBody.angularVelocity = Vector3.zero;
        mRigidBody.velocity = Vector3.zero;
        transform.localScale = Vector3.one * size;

        // 위치 설정
        transform.position = pos;

        // 텍스트 설정
        mDamageLabel.text = Mathf.Abs(Mathf.RoundToInt(amount)).ToString();
        mDamageLabel.color = color;

        // 연출을 위한 힘 추가
        mRigidBody.AddForce(new Vector3(Random.Range(-5f, 5f), 5f, Random.Range(-5f, 5f)), ForceMode.Impulse);

        if(mCoFadeLabel is not null)
            StopCoroutine(mCoFadeLabel);
        mCoFadeLabel = StartCoroutine(CoFadeLabel());
    }

    private IEnumerator CoFadeLabel()
    {
        float process = 0f;
        Vector3 initSize = transform.localScale;

        while(process < 1.0f)
        {
            process += Time.deltaTime;

            transform.localScale = Vector3.Lerp(initSize, Vector3.zero, process);
            mDamageLabel.alpha = Mathf.Lerp(1.0f, 0.0f, process);

            yield return null;
        }

        gameObject.SetActive(false);
    }
}

 

[Header("프리팹의 리지드바디")]
[SerializeField] Rigidbody mRigidBody;
  • 데미지 인디케이터 오브젝트는 충돌 검사를 하지 않는 리지드바디를 사용합니다.

 

[Header("프리팹의 데미지 라벨")]
[SerializeField] TextMeshPro mDamageLabel;
  • 데미지를 표시할 텍스트 라벨입니다.

 

[Header("프리팹의 자식 객체 (Look Camera)")]
[SerializeField] Transform mChildTransform;
  • 항상 카메라를 응시할 자식 객체입니다.

 

private void Update()
{
    mChildTransform.LookAt(Camera.main.transform);
}
  • 오브젝트가 활성화 되어있을 때 항상 카메라를 바라보게합니다.

 

public void Init(Vector3 pos, float amount, Color color, float size)
{
    // 만약 호출된 값이 0이라면 리턴
    if(Mathf.RoundToInt(amount) == 0)
    {
        gameObject.SetActive(false);
        return;
    }

    // 재사용시 기존의 상태를 초기화
    mRigidBody.angularVelocity = Vector3.zero;
    mRigidBody.velocity = Vector3.zero;
    transform.localScale = Vector3.one * size;

    // 위치 설정
    transform.position = pos;

    // 텍스트 설정
    mDamageLabel.text = Mathf.Abs(Mathf.RoundToInt(amount)).ToString();
    mDamageLabel.color = color;

    // 연출을 위한 힘 추가
    mRigidBody.AddForce(new Vector3(Random.Range(-5f, 5f), 5f, Random.Range(-5f, 5f)), ForceMode.Impulse);

    if(mCoFadeLabel is not null)
        StopCoroutine(mCoFadeLabel);
    mCoFadeLabel = StartCoroutine(CoFadeLabel());
}
  • 오브젝트 풀링에서 꺼내져 활성화 될 때, 호출되는 초기화 함수입니다.
  • 매개변수로 시작 위치, 표시할 양(데미지 및 기타), 색상과 크기를 전달받습니다.
  • amount가 0일 수 있습니다. 이럴 땐 리턴을 하여 호출을 무시합니다.

 

// 재사용시 기존의 상태를 초기화
mRigidBody.angularVelocity = Vector3.zero;
mRigidBody.velocity = Vector3.zero;
transform.localScale = Vector3.one * size;
  • 리지드바디는 비활성화가 되어도 힘 속성을 유지합니다.
  • 이 힘 속성을 초기화하여 완전히 새로운 상태로 사용할 수 있도록 합니다.
  • 스케일은 코루틴에서 점점 작아지도록 구현하였기에 값 또한 초기화해줍니다.

 

// 연출을 위한 힘 추가
mRigidBody.AddForce(new Vector3(Random.Range(-5f, 5f), 5f, Random.Range(-5f, 5f)), ForceMode.Impulse);
  • 리지드바디의 AddForce를 이용하여 힘을 줍니다.
  • X, Z 방향은 무작위 방향으로 힘을 주고, Y축으로는 고정된 5만큼의 힘을 주어 위로 떠오르면서 무작위 방향으로 날아가게 설정합니다.
  • Impulse 속성을 이용하여 순간적인 강한 힘을 부여합니다.
  • "[유니티] 충돌 연출 및 RigidBody.AddForce()의 이해" 글에서 Rigidbody.Addforce의 네가지 힘 속성을 비교할 수 있습니다.

 

private IEnumerator CoFadeLabel()
{
    float process = 0f;
    Vector3 initSize = transform.localScale;

    while(process < 1.0f)
    {
        process += Time.deltaTime;

        transform.localScale = Vector3.Lerp(initSize, Vector3.zero, process);
        mDamageLabel.alpha = Mathf.Lerp(1.0f, 0.0f, process);

        yield return null;
    }

    gameObject.SetActive(false);
}
  • 인디케이터를 자연스럽게 사라지게 합니다.
  • 크기를 서서히 줄이고, 알파값을 0으로 가깝게하여 서서히 사라지게 연출합니다.

 

· 프리팹

  • 위 이미지와 같이 구성하였습니다.
  • DamageIndicator.cs를 붙이면 자동으로 리지드바디가 생성됩니다. Gravity 속성을 켜줍니다.
  • Front로 봤을 때 좌우가 반전된 상태로 세팅합니다.
  • 그 이유는 Look Camera를 할 때, 좌우가 180도 돌아간 상태로 보여야 정상적으로 보이기 때문입니다.

 

✅ 사용

  • 오브젝트 풀을 사용하여 호출합니다.

 

· 오브젝트 풀

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DamageIndicatorPool : OBJPool<DamageIndicator, DamageIndicatorPool>
{
    /// <summary>
    /// 풀에서 비활성 데미지 인디케이터 객체를 가져오고 초기화
    /// </summary>
    /// <param name="pos"></param>
    /// <param name="amount"></param>
    /// <param name="scale"></param>
    /// <returns></returns>
    public DamageIndicator GetFromPool(Vector3 pos, float amount, Color color, float scale = 1.0f)
    {
        DamageIndicator indicator = GetFromPool();
        indicator.Init(pos, amount, color, scale);

        return indicator;
    }

    //풀(mPool)로부터 오브젝트를 가져온다.
    override public DamageIndicator GetFromPool(int id = 0)
    {
        for (int i = 0; i < mPool[id].Count; ++i)
        {
            if (!mPool[id][i].gameObject.activeInHierarchy)
            {
                mPool[id][i].gameObject.SetActive(true);
                return mPool[id][i];
            }
        }
        return MakeNewInstance(id);
    }
}
  • 오브젝트 풀링을 사용하여 비활성 객체를 가져오고 초기화합니다.
  • GetFromPool을 오버로딩하여 풀에서 가져옴과 동시에 초기화할 수 있도록 구현하였습니다.

 

· 체력

override public bool ModifyCurrentHp(float amount)
{
    bool isDead = base.StatData.ModifyCurrentHp(amount);
    EnemyInfoDraw.UpdateHPBar(base.StatData.HpCurrent / base.StatData.hpMax);

    // 데미지 인디케이터
    DamageIndicatorPool.Instance.GetFromPool(transform.position, amount, amount < 0 ? Color.red : Color.green);

    return isDead;
}
  • 플레이어에게 피격을 당한 경우 또는 회복을 한경우 FSM에서 사용하는 체력 예시입니다.
  • amount만큼 체력이 변경되는데, 이 상태에서 음수값이면 체력이 닳는 상황입니다.
  • 체력이 닳는 상황이면 Color을 red로 하고, 그것이 아니면 green으로 하여 체력을 회복하는 연출을 할 수 있습니다.

 

· 마나

override public void ModifyCurrentMp(float value)
{
    // 마나 조정
    base.StatData.ModifyCurrentMp(value);

    // 데미지 인디케이터
    DamageIndicatorPool.Instance.GetFromPool(transform.position, value, value < 0 ? Color.magenta : Color.blue);        

    // UI 업데이트
    mUIStatDraw.UpdateManaCircle(base.StatData.MpCurrent/ base.StatData.mpMax);
}
  • 플레이어가 마나를 소모하거나, 회복하는 함수입니다.
  • 마나를 소모하면 마젠타 색상, 마나를 회복하면 파란색으로 보여줍니다.
bonnate