Unity3D 멀티게이지
대충 이런거
옛날부터 체력바 같은 곳에서 많이 보던... 멀티 체력바? 멀티 게이지바? 다중 게이지바?
정확한 명칭은 모르겠지만 아무튼 멀티 게이지로 부르자.
체력바로만 쓸것도 아니고 좀 더 다양하게 쓸 수 있도록 Unity Image UI의 Fill Method에 대응하도록 만들었다.
스크립트 UI는 최대한 필요한 옵션만 남기고 나머지는 코드 내부적으로 처리하도록 했다.
옵션 설명은 Tooltip으로 적어놨기에 마우스를 대보면 나온다.
지금은 잔상의 색을 그냥 Multi Gage Color의 x0.5 한 색을 그리도록 했는데 필요하다면 살짝 수정하여 잔상 색도 사용자가 원하는 색이 되도록 할 수도 있다.
아래는 코드
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
namespace Kupa
{
public class MultiGage : MonoBehaviour
{
[System.Serializable]
public class TargetGageValue //다른 스크립트의 게이지 표시를 원하는 값을 Reference Type으로 참조하도록 클래스 생성. 지속적으로 모니터링하기 위함
{
public float value;
public TargetGageValue(float value = 0f)
{
this.value = value;
}
}
[Tooltip("게이지 타입")]
public Image.FillMethod fillMethod = Image.FillMethod.Horizontal;
[Tooltip("MultiGageColor와 무관하게 0줄 이하가 되었을 때 그릴 색")]
public Color nonValueColor = Color.black;
[Tooltip("순서대로 그려질 색")]
public Color[] multiGageColor = { Color.red, Color.yellow };
[Tooltip("한 줄의 게이지 값")]
public float gageLineValue = 10;
[Tooltip("피격 후 딤 게이지가 줄어들기 시작하는 시간")]
public float dimWaitTime = 0.1f;
[Tooltip("딤 게이지가 줄어들기 시작 후 다 줄어들때까지 걸리는 시간")]
public float dimDeleteTime = 0.3f;
[Tooltip("게이지 잔상 연출")]
public bool dimEffectOn = true;
private GameObject uiCanvasObject; //SetActive 용도
private RectTransform gage1RectTransform = null;
private RectTransform gage2RectTransform = null;
private RectTransform gageDim1RectTransform = null;
private RectTransform gageDim2RectTransform = null;
private Canvas gage1Canvas; //SortOrder 변경 용도
private Canvas gage2Canvas;
private Canvas gageDim1Canvas;
private Canvas gageDim2Canvas;
private Image gage1Image; //fillAmount 조절 용도
private Image gage2Image;
private Image gageDim1Image;
private Image gageDim2Image;
private IEnumerator gageEffectCor;
private IEnumerator gageDimEffectCor;
private TargetGageValue targetGageValue;
private float prevGageValue;
private float dimGageValue;
private int colorIndex = 0;
private bool isInit = false;
private void Awake()
{
InitProperty();
}
public void ObserveStart(TargetGageValue target)
{
targetGageValue = target;
InitProperty();
InitSetting();
CalcGage();
uiCanvasObject.SetActive(true);
dimGageValue = prevGageValue = targetGageValue.value;
if (gageEffectCor != null)
StopCoroutine(gageEffectCor);
gageEffectCor = ObserveCor();
StartCoroutine(gageEffectCor);
}
public void ObserveEnd()
{
if (gageDimEffectCor != null)
StopCoroutine(gageDimEffectCor);
if (gageEffectCor != null)
StopCoroutine(gageEffectCor);
uiCanvasObject.SetActive(false);
}
IEnumerator ObserveCor()
{
while (true)
{
if (prevGageValue != targetGageValue.value)
{
CalcGage();
if (dimEffectOn)
{
if (prevGageValue < targetGageValue.value)
{
dimGageValue = targetGageValue.value;
gageDim1Canvas.sortingOrder = 10001 + colorIndex * 2;
gageDim2Canvas.sortingOrder = 10001 + colorIndex * 2 - 2;
gageDim1Image.color = 0 <= colorIndex ? multiGageColor[colorIndex % multiGageColor.Length] * 0.5f : nonValueColor;
gageDim2Image.color = 1 <= colorIndex ? multiGageColor[(colorIndex - 1) % multiGageColor.Length] * 0.5f : nonValueColor;
gageDim1Image.fillAmount = targetGageValue.value % gageLineValue / gageLineValue;
}
if (gageDimEffectCor != null)
StopCoroutine(gageDimEffectCor);
gageDimEffectCor = GageEffectCor();
StartCoroutine(gageDimEffectCor);
}
}
prevGageValue = targetGageValue.value;
yield return null;
}
}
IEnumerator GageEffectCor()
{
yield return new WaitForSeconds(dimWaitTime);
float timer = dimDeleteTime;
int dimColorIndex;
while (0 < timer)
{
dimGageValue = Mathf.Lerp(targetGageValue.value, dimGageValue, timer / dimDeleteTime);
dimColorIndex = Mathf.FloorToInt(dimGageValue / gageLineValue);
gageDim1Canvas.sortingOrder = 10001 + dimColorIndex * 2;
gageDim2Canvas.sortingOrder = 10001 + dimColorIndex * 2 - 2;
gageDim1Image.color = 0 <= dimColorIndex ? multiGageColor[dimColorIndex % multiGageColor.Length] * 0.5f : nonValueColor;
gageDim2Image.color = 1 <= dimColorIndex ? multiGageColor[(dimColorIndex - 1) % multiGageColor.Length] * 0.5f : nonValueColor;
gageDim1Image.fillAmount = dimGageValue % gageLineValue / gageLineValue;
yield return null;
timer -= Time.deltaTime;
}
gageDim1Canvas.sortingOrder = 10001 + colorIndex * 2;
gageDim2Canvas.sortingOrder = 10001 + colorIndex * 2 - 2;
gageDim1Image.color = 0 <= colorIndex ? multiGageColor[colorIndex % multiGageColor.Length] * 0.5f : nonValueColor;
gageDim2Image.color = 1 <= colorIndex ? multiGageColor[(colorIndex - 1) % multiGageColor.Length] * 0.5f : nonValueColor;
gageDim1Image.fillAmount = targetGageValue.value % gageLineValue / gageLineValue;
}
private void InitProperty()
{
if (isInit) return;
uiCanvasObject = transform.GetChild(0).gameObject;
gage1RectTransform = uiCanvasObject.transform.Find("Gage1").GetComponent<RectTransform>();
gage2RectTransform = uiCanvasObject.transform.Find("Gage2").GetComponent<RectTransform>();
gageDim1RectTransform = uiCanvasObject.transform.Find("GageDim1").GetComponent<RectTransform>();
gageDim2RectTransform = uiCanvasObject.transform.Find("GageDim2").GetComponent<RectTransform>();
gage1Canvas = gage1RectTransform.GetComponent<Canvas>();
gage2Canvas = gage2RectTransform.GetComponent<Canvas>();
gageDim1Canvas = gageDim1RectTransform.GetComponent<Canvas>();
gageDim2Canvas = gageDim2RectTransform.GetComponent<Canvas>();
gage1Image = gage1RectTransform.GetComponent<Image>();
gage2Image = gage2RectTransform.GetComponent<Image>();
gageDim1Image = gageDim1RectTransform.GetComponent<Image>();
gageDim2Image = gageDim2RectTransform.GetComponent<Image>();
isInit = true;
}
private void InitSetting()
{
gage1Canvas.overrideSorting = gage2Canvas.overrideSorting = gageDim1Canvas.overrideSorting = gageDim2Canvas.overrideSorting = true;
gage1Image.fillMethod = gage2Image.fillMethod = gageDim1Image.fillMethod = gageDim2Image.fillMethod = fillMethod;
gageDim1Canvas.sortingOrder = gageDim2Canvas.sortingOrder = 10000;
gageDim1Image.color = gageDim2Image.color = nonValueColor;
gageDim1Image.fillAmount = gageDim2Image.fillAmount = 0;
gage2Image.fillAmount = gageDim2Image.fillAmount = 1;
}
private void CalcGage()
{
colorIndex = Mathf.FloorToInt(targetGageValue.value / gageLineValue);
gage1Canvas.sortingOrder = 10002 + colorIndex * 2;
gage2Canvas.sortingOrder = 10002 + colorIndex * 2 - 2;
gage1Image.color = 0 <= colorIndex ? multiGageColor[colorIndex % multiGageColor.Length] : nonValueColor;
gage2Image.color = 1 <= colorIndex ? multiGageColor[(colorIndex - 1) % multiGageColor.Length] : nonValueColor;
gage1Image.fillAmount = targetGageValue.value % gageLineValue / gageLineValue;
}
private void CalcGageDim()
{
gageDim1Canvas.sortingOrder = 10001 + colorIndex * 2;
gageDim2Canvas.sortingOrder = 10001 + colorIndex * 2 - 2;
gageDim1Image.color = 0 <= colorIndex ? multiGageColor[colorIndex % multiGageColor.Length] * 0.5f : nonValueColor;
gageDim2Image.color = 1 <= colorIndex ? multiGageColor[(colorIndex - 1) % multiGageColor.Length] * 0.5f : nonValueColor;
gageDim1Image.fillAmount = targetGageValue.value % gageLineValue / gageLineValue;
}
}
}
가능하면 Inspector 창을 간단하게 하기 위해 컴포넌트 참조 과정은 코드 상에서 처리했다.
반대로 말하면 지정된 계층구조가 깨지면 작동하지 않는단 소리지만 프리팹으로 만들어서 사용하면 문제 없다.
만약 여러 UI 형태가 필요하여 한 스크립트에 동일하게 적용할 수 없다면 컴포넌트 필드를 public이나 SerializeField 어트리뷰트를 부여하여 엔진에서 직접 연결해주자.
현재 이 코드에 최적화 된 프리팹은 아래와 같다.
먼저 계층 구조는 심플하다.
스크립트가 들어갈 최상위.
Test 스크립트는 멀티 게이지가 동작하기 위해 임시로 넣은 테스트 스크립트다.
TargetGageValue 하나 생성 후 ObserveStart에 인자로 넣고 호출하는게 전부다.
이후엔 실시간으로 TargetGageValue 값의 변화를 확인하고 게이지가 변동된다.
Canvas는 본인이 사용할 Canvas 옵션을 두면 된다. WorldSpace에서 쓰든, Overlay로 쓰든 상관없다.
Gage1, Gage2, GageDim1, GageDim2은 위와 동일하다. 기본적으로 사용자는 RectTransform으로 형태만 본인 입맛에 맞게 조절해주면 되고 Image와 Canvas 컴포넌트는 달려만있으면 된다. Image와 Canvas 컴포넌트의 설정값은 코드에서 변경해준다.
제일 중요한건 오브젝트의 이름인데 코드가 해당 Name으로 Find 하고있기 때문에 꼭 바꾸고 싶다면 코드랑 같이 바꿔줘야한다.
여기선 기본적인 기능만을 달아두었기에 커스텀이 필요하다면 수정해서 사용토록 하자.