Unity UI Toolkit 게임 UI
UXML로 구조를, USS로 모양을, 게임 이벤트에 반응하는 소규모 C# 컨트롤러로 동작을 정의합니다. 실전 프로젝트(호러 슈터 HUD)에서 추출되었습니다. 문제 해결 목록이 가장 시간을 절약해주는 부분입니다.
1. 코드 작성 전
한 번에 짧게 묶어서 누락된 것만 물어보세요:
- UI API.
[RequireComponent(typeof(PanelRenderer))]+RegisterUIReloadCallback= PanelRenderer 워크플로우(UIDocument없음,rootVisualElement없음).UIDocument+rootVisualElement= 클래식 방식. 붙여넣은 코드로 유추하고, 불명확하면 작동하는 컨트롤러 하나와 그 UXML을 요청하세요. 절대 두 방식을 혼합하지 마세요(NullReferenceException 발생). 사용자의 실제 작동 파일이 API 기억보다 낫습니다. - 장르/테마(스크린샷이나 팔레트 포함), 플랫폼 및 입력, 참조 해상도/종횡비.
- UI가 수신하는 게임 스크립트. 스크립트를 요청하고, 실제 공개 이벤트/프로퍼티를 읽고, 그것들에 바인딩하세요. 이벤트가 없으면 게임 스크립트에 가장 작은 추가를 제안하세요.
- 사용자의 언어로 답변하고, 기존 스크립트가 사용하는 언어로 코드 주석을 작성하세요.
2. 스타일 가이드 먼저
일관성은 사용자가 가장 먼저 알아차리는 부분입니다. 여러 부품을 만들기 전에: 짧은 가이드를 제안하고, 하나의 작은 부품을 만든 후 사용자의 반응을 확인하고 피드백을 규칙("배경 박스 없음", "더 컴팩트하게")으로 전환하세요. 프로젝트에 저장하고(예: Assets/UI/UI_STYLE_GUIDE.md), 향후 UI 요청에 붙여넣도록 사용자에게 알리세요. 부품이나 규칙이 변경될 때 업데이트하세요. 내용: 디자인 언어(톤, 컨테이너, 밀도, 모션), 팔레트 표(텍스트, 음소거, 경고, 위험, 그림자), 타이포그래피(역할별 크기, 그림자 규칙), 레이아웃(영역, 여백, 패널 설정), USS 명명 및 상태 클래스, 코드 아키텍처 규칙, UXML 주의사항, 기존 부품 목록, 새 부품을 위한 체크리스트.
3. 아키텍처
- 하나의 부품 = 하나의 컨트롤러 + 하나의 UXML + 하나의 USS + 하나의 데이터 소스. 메가 컨트롤러는 없습니다.
- 함께 있어야 하는 부품은 하나의 UXML/flex 컨테이너를 공유합니다(두 컨트롤러가 하나의 GameObject/PanelRenderer를 공유할 수 있음; 각자 이름으로 자신의 요소를 찾습니다).
- 컨트롤러는 요소를 찾고, 텍스트/값을 설정하고, USS 클래스를 토글만 합니다. 색상, 크기 및 애니메이션은 USS에 있습니다.
- PanelRenderer:
RegisterUIReloadCallback(OnUIReload)을 통해 루트가 도착합니다(OnEnable에서 등록, OnDisable에서 해제). UI가 준비될 때와 모든 리로드 시에 실행되므로, 모든Q<T>("name")조회, 캐싱, 이벤트 바인딩 및 상태 재설정이 거기서 이루어집니다. 요소가 존재할 때까지Update()를 Null 가드하세요. - 게임 이벤트는 UI가 존재하기 전에 발생할 수 있습니다: 최신 값을 필드에 캐시하고
OnUIReload끝에서 다시 적용하세요. - 플레이어/게임 오브젝트가 늦게 스폰될 수 있습니다: 직렬화된 참조, 또는
FindFirstObjectByType을 스로틀링(~0.5초)한 후 바인딩하고, OnDisable에서 구독을 해제하세요. - Push, not poll. 이벤트를 구독하고 변경 시에만 작성하세요. 이벤트가 없는 값(지속적인 스태미나, 재장전 bool)만 폴링하고, 변경 시에만 클래스를 토글하세요. 프레임당 문자열 생성을 피하세요.
- 반복되는 요소(pips, segments)는 코드에서 한 번 빌드하고, 소스가 변경될 때만 다시 빌드하며, 리로드 시 캐시된 목록을 지우세요.
- 타이머/펄스:
element.schedule.Execute(...).Every(ms)는 일시 중지된 상태로 생성되고, 필요할 때만 재개됩니다. 원샷 플래시:.StartingIn(ms). - HUD 요소: 모든 곳에서
picking-mode="Ignore", 포커스 가능한 컨트롤 없음. 메뉴는 예외입니다. - 파일 이름 = MonoBehaviour 클래스 이름.
4. 레이아웃 (시각적 버그의 주요 원인)
- 별도로 추측한 픽셀 오프셋으로 관련 부품을 배치하지 마세요: 두 개의 절대 패널이
bottom을 맹목적으로 추정하여 겹친 경우가 있었습니다. 하나의 flex 컨테이너에 넣고 해당 컨테이너만 위치시키세요. - 화면 영역당 하나의 절대 위치 컨테이너; 자식은 flex와 여백으로 흐릅니다.
- 레이아웃이 점프하지 않아야 할 때는
opacity+ 전환으로 숨기고, 공간이 축소되어야 할 때만display:none을 사용하세요. - 숫자 레이블에 고정
width를 사용하여 9에서 10, 100으로 변경될 때 이웃이 움직이지 않도록 하세요. - 박스는 border-box입니다: 세그먼트 행 너비 =
개수 x (너비 + 여백) - 여백. 동일한 왼쪽 가장자리와 계산된 너비로 관련 위젯을 정렬하세요. - 패널 설정: Scale With Screen Size(예: 1920x1080, match 0.5); HUD의 경우 Clear Color와 Clear Depth Stencil을 OFF로 설정하세요. PanelRenderer의 Sort Order를 사용하여 레이어 부품(전체 화면 비네트는 텍스트 아래에)을 배치하세요.
5. USS 및 상태
- 상태 = C#에서 토글된 클래스(
EnableInClassList); 애니메이션 = USS 전환. 코드에서 색상을 설정하거나 애니메이션을 적용하지 마세요. - 의미별로 상태 이름 지정:
--low,--critical,--hidden,--hit,--pulse,--unavailable. 새 부품에는 BEM을 선호합니다. - 재정의 규칙(소모된 세그먼트, 플래시)은 재정의하는 규칙보다 먼저 와야 합니다; 동일한 특이성, 마지막이 승리합니다.
- 전환은 대상 상태의 지속 시간/지연을 사용합니다:
.x--hidden { opacity:0; transition-duration:.5s; transition-delay:.9s; }는 늦게 사라지고 즉시 나타납니다;.x--flash { transition-duration:.05s; }는 켜지고 기본 속도로 꺼집니다. - 배경 없이 떠 있는 텍스트는
text-shadow가 필요합니다.text-transform없음(C#에서 대문자 처리); 방사형 그라데이션 없음(비네트 참조). - 재스킨 가능한 UI의 경우 루트 클래스에 USS 변수(
--c-text,var(--c-text)로 사용)로 색상/크기를 넣고, 사용자의 Unity 버전이 이를 지원하는지 확인하세요.
6. 컨트롤러 스켈레톤 (PanelRenderer)
[RequireComponent(typeof(PanelRenderer))]
public class PartController : MonoBehaviour
{
[SerializeField] private SourceComponent source; // 비어 있으면 런타임에 찾기
private PanelRenderer _pr; private VisualElement _block; private Label _value;
private bool _bound; private float _v = -1f; private int _shown = -1; private float _nextFind;
private void Awake() => _pr = GetComponent<PanelRenderer>();
private void OnEnable() { _pr.RegisterUIReloadCallback(OnUIReload); TryBind(); }
private void OnDisable() { _pr.UnregisterUIReloadCallback(OnUIReload); Unbind(); _block = null; _value = null; }
private void Update()
{
if (_bound || Time.unscaledTime < _nextFind) return;
_nextFind = Time.unscaledTime + 0.5f; TryBind();
}
private void TryBind()
{
if (_bound) return;
if (source == null) source = FindFirstObjectByType<SourceComponent>();
if (source == null) return;
source.OnChanged += OnChanged; _bound = true;
_v = source.Current; Apply(); // 현재 상태를 한 번 가져오기
}
private void Unbind() { if (_bound && source != null) source.OnChanged -= OnChanged; _bound = false; }
private void OnUIReload(PanelRenderer r, VisualElement root)
{
_block = root.Q<VisualElement>("part"); _value = root.Q<Label>("part-value");
if (_block == null || _value == null) { Debug.LogError("Part: UXML에서 요소를 찾을 수 없습니다(Source Asset 확인).", this); _block = null; return; }
_shown = -1; Apply(); // 새 트리: 캐시에서 강제로 다시 쓰기
}
private void OnChanged(float v) { _v = v; Apply(); }
private void Apply()
{
if (_block == null || _v < 0f) return;
int n = Mathf.CeilToInt(_v);
if (n != _shown) { _shown = n; _value.text = n.ToString(); } // 변경 시에만 쓰기
_block.EnableInClassList("part--low", _v <= 0.3f);
}
}
7. 재사용 가능한 스니펫
- 세그먼트/핍:
OnUIReload에서row.Clear(); list.Clear();후 N개의VisualElement{pickingMode=Ignore}를seg클래스로 추가; 변경 시list[i].EnableInClassList("seg--spent", i >= filled). 값이 0보다 큰 동안 최소 1개 채워진 상태 표시:filled = v<=0 ? 0 : Clamp(CeilToInt(v*N-0.001f),1,N). - 펄스:
_pulse = _block.schedule.Execute(() => _block.ToggleInClassList("part--pulse")).Every(400); _pulse.Pause();상태 진입 시 재개, 떠날 때 Pause + 클래스 제거;_pulsingbool을 추적하고 리로드 시 재설정. - 이벤트 시 플래시:
var b=_block; b.AddToClassList("part--hit"); b.schedule.Execute(()=>b.RemoveFromClassList("part--hit")).StartingIn(150); - 비네트: Awake에서 한 번 128x128
Texture2D(RGBA32, Clamp, Bilinear,HideFlags.HideAndDontSave) 생성. 알파는 중심에서SmoothStep(0,1,InverseLerp(inner,outer,dist/1.4142)), 흰색 RGB; OnUIReload에서_el.style.backgroundImage = new StyleBackground(tex)설정; OnDestroy에서 Destroy. USS: 전체 화면 절대 요소,opacity:0,-unity-background-scale-mode: stretch-to-fill,-unity-background-image-tint-color(색상용), 클래스--low/--critical/--pulse/--flash는 opacity 설정(flash는 마지막, 빠른 지속 시간). 텍스트 부분보다 낮은 Sort Order.
8. 장르 및 플랫폼
아키텍처는 모든 장르에서 동일합니다. 스타일 가이드와 입력 모델이 변경됩니다. 아케이드: 크고 굵은 숫자, 카운트 업, 스케일 팝, 콤보 글로우, 펀치감 있는 0.08-0.25초 전환, 컨테이너 환영. 레트로/픽셀: 픽셀 폰트, 정수 스케일링, Point 필터 텍스처, 스프라이트 9슬라이스 테두리. 모바일: 터치 대상 약 44-48 dp, 디바이스 안전 영역을 루트 패딩으로(사용자의 Unity 버전에 맞는 현재 API 확인), 호버 상태 없음, 더 큰 최소 글꼴, 실제 기기에서 테스트. 콘솔/게임패드: 강력한 :focus 스타일이 있는 포커스 가능 요소, 명시적 초기 포커스, TV 안전 여백(3-5%), 더 큰 텍스트, 활성 장치로 전환되는 글리프. VR/월드 스페이스: Unity 버전에 따라 다름, 먼저 조회하세요. 이러한 플랫폼 포인트는 건전한 일반적인 관행이지만 실제 프로젝트에서는 테스트되지 않았습니다: 그렇게 말하고 사용자에게 기기에서 확인하도록 요청하세요. 입력 레이블(키 이름 또는 글리프)은 컨트롤러가 읽는 데이터에 유지하고, UXML에 하드코딩하지 마세요.
9. 부품 전달
순서대로: 완전한 UXML, 완전한 USS, 완전한 컨트롤러, 설정 단계(GameObject, PanelRenderer + 공유 Panel Settings + Source Asset, Sort Order, 채울 참조, Play에서 볼 내용), 튜닝 노브(크기/위치/색상/임계값을 변경하는 USS 값), 그리고 짧은 스타일 가이드 항목. 기존 파일을 변경할 때는 파일의 대부분이 변경되는 경우 전체 파일을 제공하세요(부분 붙여넣기는 실제 오류를 발생시켰습니다).
10. 문제 해결
| 증상 | 원인 / 해결책 |
|---|---|
rootVisualElement / GetComponent<UIDocument>()에서 NullReference | 코드가 UIDocument를 사용하지만 오브젝트에 PanelRenderer가 있음. RegisterUIReloadCallback을 사용하고 OnUIReload에서 바인딩 |
| 컨트롤러가 "요소를 찾을 수 없음" 로그 출력 | 잘못된 Source Asset, Q<>()와 이름 불일치, 또는 잘못된 UXML |
| UI Builder "Failed to open asset" | 잘못된 UXML(중복 </ui:UXML>, 잘못된 <Style>, 플레이스홀더 GUID, 숨겨진 문자). 수정 후 파일 우클릭 > Reimport. 일반 오류 위의 콘솔 메시지를 읽으십시오 |
| 스타일이 적용되지 않은 요소 | <Style>이 없거나 경로가 잘못됨. UI Builder에서: StyleSheets > + > Add Existing USS(project://database/...guid=... 전체 경로를 씀) |
| 편집 후 모든 것이 사라짐 | UXML이 손상되었거나, 이름 변경 후 Source Asset 참조가 오래됨. 수정, Reimport, 재할당. 루트에 background-color: rgba(255,0,0,.3)로 테스트 |
| 부품이 다른 부품과 잘못 배치되거나 겹침 | 오래된 GameObject가 여전히 이전 UXML을 그리고 있거나, 별도의 절대 오프셋 사용. 이전 오브젝트 삭제, 이웃을 하나의 컨테이너로 병합 |
| Game 뷰에 플레이스홀더 텍스트 | 편집 모드에서 UXML을 그림. Play 누르기 |
| 스크립트를 추가할 수 없음 | 파일 이름 != 클래스 이름. Unity Project 창 내에서 이름을 변경하여 .meta가 따라오도록 함 |
유효하지 않은 수정: stylesheet 태그는 ui: 접두사 없이 <Style src="..."/>입니다(<ui:Style>는 조용히 무시됨). 플레이스홀더 GUID를 절대 만들거나 남겨두지 마십시오. 표준 헤더 <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">를 사용하십시오.
11. 에디터를 보지 않고 작업하기
레이아웃 불만 사항에 대해 스크린샷을 요청하고 자세히 읽으십시오. 추측으로는 해결할 수 없었던 버그를 해결했습니다. 자신의 실수를 간결하게 인정하고 완전한 파일로 수정하십시오. 컴파일 오류의 경우 다시 추측하는 대신 정확한 텍스트와 Unity 버전을 요청하십시오. 기기 동작이나 성능 수치를 약속하지 마십시오. 구조를 만들고 사용자에게 테스트를 요청하십시오.