오토 배틀러 장르 개인작 개발중에, 특성상 다양한 캐릭터가 한 화면에 등장하고 최대 체력과 현재 체력을 쉽고 빠르게 식별하기 위해 체력바를 가시성 좋게 고쳐야 할 필요성을 느꼈습니다.
기존에 사용하던 캐릭터 체력바는 간단히 비율만 표기했었습니다.
캐릭터의 체력은 500에서 5000을 넘나드는데, 숫자로 표기하거나 추가적인 창을 띄워 보는것은 덕지덕지 불편할 수 있죠. 그렇다면 기존 게임들은 어떻게 해결하고 있을까요?
리그 오브 레전드는 한 칸당 100을 의미합니다.
전략적 팀 전투는 한 칸당 300을 의미하고요.
스타크래프트는 칸으로 나누었지만, 칸당 수치가 고정되어있지는 않습니다.
수치당 칸과 색으로 표기해서 가시성을 높였습니다.
제가 작업하고자 하는 부분은 셰이더로 체력 셀을 나눠주는 부분입니다.
이런 모양으로요.
Health Bar 는 4개의 이미지 컴포넌트로 구성되어있습니다.
가장 아래쪽에 쉴드, 그 위에 데미지를 받아 변경된 수치를 업데이트 해 줄 이펙트용 이미지, 실제 체력, 그리고 가장 위에서 몇 칸인지 나누어질 Separator입니다.
Separator를 제외하고는 단순하게 Image 컴포넌트만으로 구현되며, Separator는 커스텀 UI 셰이더로 칸막이를 그려줄겁니다.
셰이더에서는 몇 가지 수치를 프로퍼티로 받고요, 스크립트에서 업데이트 해 줄 겁니다.
Shader "ABS/UI/Health Separator"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (0,0,0,1)
_Steps ("Steps", Float) = 1
_HSRatio ("Hp-Shield Ratio", Range(0,1)) = 1
_Width ("Rect Width", Int) = 160
_Thickness ("Thickness", Float) = 1
[MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
[Toggle(CLOSE_BAR_ON)] CloseBar ("Close Bar", Float) = 1
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend One OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#pragma multi_compile _ PIXELSNAP_ON
#pragma multi_compile _ CLOSE_BAR_ON
#include "UnityCG.cginc"
#include "UnityUI.cginc"
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
half4 mask : TEXCOORD2;
UNITY_VERTEX_OUTPUT_STEREO
};
sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float4 _MainTex_ST;
float _UIMaskSoftnessX;
float _UIMaskSoftnessY;
half _Steps;
half _HSRatio;
half _Thickness;
half _Width;
v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
float4 vPosition = UnityObjectToClipPos(v.vertex);
OUT.worldPosition = v.vertex;
// 픽셀 스냅 기능 추가함
#ifdef PIXELSNAP_ON
OUT.vertex = UnityPixelSnap(vPosition);
#else
OUT.vertex = vPosition;
#endif
float2 pixelSize = vPosition.w;
pixelSize /= float2(1, 1) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
float2 maskUV = (v.vertex.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);
OUT.texcoord = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
OUT.mask = half4(v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy)));
OUT.color = v.color * _Color;
return OUT;
}
fixed4 frag(v2f IN) : SV_Target
{
half4 c = IN.color * (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd);
// 그리지 않음
if (IN.texcoord.x > _HSRatio)
{
c.a = 0;
}
else
{
#ifdef CLOSE_BAR_ON
const half width = _Width * _HSRatio - _Thickness;
#else
const half width = _Width * _HSRatio;
#endif
const half x = IN.texcoord.x * _Width;
const half step = _Steps + 0.000000001;
const half cell = width / step;
// uv.x로 가로 위치를 구하고 한 칸 길이로 나눈 나머지가 두께보다 적으면 색칠.
// 각 셀의 왼쪽에 바가 그려진다.
if (x % cell > cell - _Thickness)
{
c = _Color;
}
else
{
c.a = 0;
}
}
c.rgb *= c.a;
#ifdef UNITY_UI_CLIP_RECT
half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.mask.xy)) * IN.mask.zw);
c.a *= m.x * m.y;
#endif
#ifdef UNITY_UI_ALPHACLIP
clip (c.a - 0.001);
#endif
return c;
}
ENDCG
}
}
}
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
namespace ACF.Tests
{
// [ExecuteAlways]
[RequireComponent(typeof(Image))]
public class HealthBarTest : MonoBehaviour
{
private const string STEP = "_Steps";
private const string RATIO = "_HSRatio";
private const string WIDTH = "_Width";
private const string THICKNESS = "_Thickness";
private static readonly int floatSteps = Shader.PropertyToID(STEP);
private static readonly int floatRatio = Shader.PropertyToID(RATIO);
private static readonly int floatWidth = Shader.PropertyToID(WIDTH);
private static readonly int floatThickness = Shader.PropertyToID(THICKNESS);
[Range(0, 2800f)] public float Hp = 1000f;
[Range(0, 2800f)] public float MaxHp = 1000f;
[Range(0, 920f)] public float Sp = 0f;
[Range(0, 10f)] public float speed = 3f;
public float hpShieldRatio;
public float RectWidth = 100f;
[Range(0, 5f)]public float Thickness = 2f;
public Image hp;
public Image damaged;
public Image sp;
public Image separator;
[ContextMenu("Create Material")]
private void CreateMaterial()
{
// if (separator.material == null)
{
separator.material = new Material(Shader.Find("ABS/UI/Health Separator"));
}
}
private IEnumerator Start()
{
yield return new WaitForSeconds(2.0f);
Hp = 1500;
MaxHp = 1500;
Sp = 400;
while (Sp > 0)
{
Sp -= 280 * Time.deltaTime;
yield return null;
}
Sp = 0;
yield return new WaitForSeconds(2f);
for (int i = 0; i < 8; i++)
{
Hp -= 120;
yield return new WaitForSeconds(1f);
}
for (int i = 0; i < 8; i++)
{
MaxHp += 200;
Hp = MaxHp;
yield return new WaitForSeconds(1f);
}
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#endif
}
private void Update()
{
if (MaxHp < Hp)
{
MaxHp = Hp;
}
float step;
// 쉴드가 존재 할 때
if (Sp > 0)
{
// 현재체력 + 쉴드 > 최대 체력
if (Hp + Sp > MaxHp)
{
hpShieldRatio = Hp / (Hp + Sp);
sp.fillAmount = 1f;
step = (Hp) / 300f;
hp.fillAmount = Hp / (Hp + Sp);
}
else
{
sp.fillAmount = (Hp + Sp) / MaxHp;
hpShieldRatio = Hp / MaxHp;
step = Hp / 300f;
hp.fillAmount = Hp / MaxHp;
}
}
else
{
sp.fillAmount = 0f;
step = MaxHp / 300f;
hpShieldRatio = 1f;
hp.fillAmount = Hp / MaxHp;
}
// sp.fillAmount = 1 - hpShieldRatio;
damaged.fillAmount = Mathf.Lerp(damaged.fillAmount, hp.fillAmount, Time.deltaTime * speed);
separator.material.SetFloat(floatSteps, step);
separator.material.SetFloat(floatRatio, hpShieldRatio);
separator.material.SetFloat(floatWidth, RectWidth);
separator.material.SetFloat(floatThickness, Thickness);
}
}
}
적용 결과
체력을 카로셀 기물들은 1/1로 설정해두어 제대로 셀이 보이지 않네요.
'Unity > 작업방식' 카테고리의 다른 글
Unity: 패키지 매니저 상대 경로 사용하기 (0) | 2022.05.08 |
---|---|
Rider: 네임스페이스 이동 리팩토링하기 (0) | 2022.01.04 |
dotnet: BannedApiAnalyzer 도입기 (0) | 2021.11.23 |
Unity: 스크립트로 사용중인 그래픽스 아키텍처 파악하기 (0) | 2021.10.20 |
Unity: 로그 파일 기본 경로 (0) | 2021.08.24 |