전투 게임에서 적이 맞았는지 즉시 알아보이게 만드는 가장 단순한 방법은 머티리얼 색을 바꾸는 것이다. 하지만 이 방식은 캐릭터마다 머티리얼 구조가 다르면 관리가 지저분해지고, SkinnedMeshRenderer가 여러 개로 나뉜 보스 몬스터에서는 누락이 자주 생긴다.
URP에서는 이런 효과를 렌더링 레이어와 Renderer Feature로 분리하면 훨씬 안정적이다. 게임 로직은 "이 오브젝트를 잠깐 강조한다"만 말하고, 실제 외곽선이나 흰색 플래시는 별도 Render Pass가 처리한다. 오늘은 적 피격 하이라이트를 전역 후처리처럼 다루는 구조를 정리한다.
1. 전용 Layer 만들기
먼저 Unity Layer에 HitHighlight를 추가한다. 피격 중인 적은 짧은 시간 동안 이 레이어로 이동시키고, Renderer Feature는 이 레이어만 다시 그린다. 원본 머티리얼을 건드리지 않기 때문에 캐릭터 셰이더, 스킨, 장비 조합과 충돌이 적다.
using UnityEngine;
using System.Collections;
public sealed class HitHighlight : MonoBehaviour
{
[SerializeField] private int highlightLayer = 12;
[SerializeField] private float duration = 0.08f;
private int _originalLayer;
private Coroutine _routine;
public void Flash()
{
if (_routine != null)
StopCoroutine(_routine);
_routine = StartCoroutine(FlashRoutine());
}
private IEnumerator FlashRoutine()
{
_originalLayer = gameObject.layer;
SetLayerRecursive(transform, highlightLayer);
yield return new WaitForSeconds(duration);
SetLayerRecursive(transform, _originalLayer);
_routine = null;
}
private static void SetLayerRecursive(Transform root, int layer)
{
root.gameObject.layer = layer;
foreach (Transform child in root)
SetLayerRecursive(child, layer);
}
}
실전에서는 사망, 풀 반환, 씬 전환 시 레이어 복구가 빠지지 않게 OnDisable에서도 원래 레이어로 돌리는 편이 좋다.
2. Renderer Feature로 별도 패스 추가
핵심은 FilteringSettings로 특정 레이어만 골라 다시 렌더링하는 것이다. 아래 예시는 선택된 레이어를 단색 머티리얼로 덮어 그리는 가장 작은 구조다.
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public sealed class HitHighlightFeature : ScriptableRendererFeature
{
[SerializeField] private LayerMask targetLayer;
[SerializeField] private Material overrideMaterial;
private HitHighlightPass _pass;
public override void Create()
{
_pass = new HitHighlightPass(targetLayer, overrideMaterial)
{
renderPassEvent = RenderPassEvent.AfterRenderingOpaques
};
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (overrideMaterial == null)
return;
renderer.EnqueuePass(_pass);
}
private sealed class HitHighlightPass : ScriptableRenderPass
{
private readonly FilteringSettings _filtering;
private readonly Material _material;
private readonly ShaderTagId _shaderTag = new("UniversalForward");
public HitHighlightPass(LayerMask layerMask, Material material)
{
_filtering = new FilteringSettings(RenderQueueRange.opaque, layerMask);
_material = material;
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
var sort = renderingData.cameraData.defaultOpaqueSortFlags;
var draw = CreateDrawingSettings(_shaderTag, ref renderingData, sort);
draw.overrideMaterial = _material;
context.DrawRenderers(
renderingData.cullResults,
ref draw,
ref _filtering
);
}
}
}
URP Renderer Asset에 이 Feature를 추가하고 targetLayer를 HitHighlight로 지정하면 된다. 패스 타이밍은 목적에 따라 바꾼다. 피격 플래시는 AfterRenderingOpaques, 벽 뒤 실루엣 강조는 AfterRenderingTransparents나 Depth 설정을 조정하는 식이다.
3. 단색 머티리얼은 최대한 단순하게
overrideMaterial은 복잡할 필요가 없다. Unlit Shader Graph로 흰색 또는 붉은색을 출력하고, ZWrite는 끄거나 상황에 맞게 둔다. 외곽선까지 만들고 싶다면 같은 레이어를 한 번 더 그리되 버텍스를 노멀 방향으로 살짝 밀어낸 셰이더를 사용한다.
public sealed class DamageReceiver : MonoBehaviour
{
private HitHighlight _highlight;
private void Awake()
{
_highlight = GetComponent<HitHighlight>();
}
public void ApplyDamage(int damage)
{
// 체력 계산, 경직, 사운드 재생 등은 생략
_highlight?.Flash();
}
}
중요한 점은 데미지 시스템이 렌더링 구현을 몰라도 된다는 것이다. 나중에 하이라이트를 흰색 플래시에서 외곽선, 디졸브, XRay 효과로 바꾸더라도 ApplyDamage 쪽 코드는 그대로 유지된다.
4. 성능 체크 포인트
Renderer Feature는 결국 대상을 한 번 더 그리는 방식이다. 적이 3마리일 때는 부담이 작지만, 수십 마리에게 동시에 적용하면 드로우콜과 스킨 메시 비용이 늘어난다. 그래서 피격 하이라이트는 지속 시간을 짧게 두고, 보스처럼 메시가 많은 캐릭터는 루트 단위로 필요한 Renderer만 별도 레이어에 넣는 최적화가 필요하다.
모바일에서는 전체 화면 후처리보다 이런 선택 렌더링이 더 예측 가능한 경우가 많다. 다만 투명 오브젝트, 파티클, 커스텀 셰이더가 섞인 캐릭터는 패스 순서와 Depth 테스트를 반드시 실기기에서 확인해야 한다.
결론
URP Renderer Feature를 쓰면 피격 하이라이트를 캐릭터 머티리얼 수정이 아니라 렌더링 파이프라인의 별도 단계로 분리할 수 있다. 게임 로직은 레이어 전환만 담당하고, 표현 방식은 Render Pass와 overrideMaterial이 맡는다. 이 구조는 적 피격 플래시, 타겟팅 표시, 상호작용 가능한 오브젝트 강조처럼 "특정 대상만 다시 그리는 효과"에 재사용하기 좋다.
'Unity·Unreal 실전' 카테고리의 다른 글
| Unreal Enhanced Input 실전: Modifier와 Trigger로 입력 로직을 코드에서 데이터로 빼기 (0) | 2026.06.15 |
|---|---|
| Unity UI Toolkit 실전 — IMGUI 없이 커스텀 EditorWindow 제작하기 (0) | 2026.06.11 |
| Unity PropertyDrawer 실전 — 커스텀 Attribute로 Inspector 필드를 직접 디자인하기 (0) | 2026.06.04 |
| Unity DOTS 실전: IJobEntity + Burst 로 수만 개 엔티티 이동을 메인 스레드에서 떼어내기 (0) | 2026.06.01 |
| Unity AssetPostprocessor로 임포트 설정 자동화하기 — 팀 전체의 텍스처 실수를 코드로 막는 법 (0) | 2026.05.28 |