장비 패널은 지금 들고 있는 도구의 이름을 띄운다. "빗자루", "안쪽 채우기 롤러" 같은 것. 이번 작업은 그 이름을 어디에 두느냐 였다.
시작은 이 한 줄이었다.
enum ECWToolType 에 이미 한글 이름이 있는데,
WBP_EquipmentPanel 의 Switch 가 같은 문구를 두 번째로 적고 있다.
같은 정보가 두 곳에 있으면, 도구를 추가할 때 두 곳을 고쳐야 하고 한쪽을 빠뜨리면 화면에 빈칸이 나온다. 그걸 한 곳으로 모으는 작업이다.
| 주제 | 문제 | 해결 |
|---|---|---|
| 도구 이름 | enum 과 BP Switch 두 곳에 중복 | DataAsset 한 곳으로 |
| 패키징 | UMETA 이름이 영문으로 떨어짐 | 런타임 문구를 FText 로 분리 |
| 누락 감지 | enum 추가 시 조용히 빈칸 | IsDataValid 로 저장 시 검출 |
| 컴포넌트 참조 | Construct 캐싱 → 리스폰 후 낡음 | 매 프레임 다시 조회 |
처음엔 간단해 보였다. enum 에 이미 이름이 있으니까.
enum class ECWToolType : uint8
{
Broom UMETA(DisplayName = "빗자루"),
Liner UMETA(DisplayName = "라인 테두리 도구"),
...
};
UEnum::GetDisplayNameTextByValue() 로 이걸 그냥 꺼내 쓰면 될 줄 알았다. 그런데 엔진 소스를 보니 안 된다.
// UEnum::GetDisplayNameTextByIndex
#if WITH_EDITOR
// 에디터에서만 DisplayName 메타데이터를 읽는다
FText LocalizedDisplayName = GetMetaDataText(TEXT("DisplayName"), ...);
if (!LocalizedDisplayName.IsEmpty())
{
return LocalizedDisplayName;
}
#endif
// 에디터가 아니면 내부 이름을 사람이 읽기 좋게 변환
return FText::FromString(FName::NameToDisplayString(RawName, bIsBool));
DisplayName 을 읽는 경로가 #if WITH_EDITOR 안에 있다. 에디터와 PIE 에서는 한글로 잘 나오지만, 패키징 빌드에서는 이 경로를 안 타서 내부 이름으로 떨어진다.
빗자루 → Broom
안쪽 채우기 롤러 → WhiteRoller
우리 프로젝트에는 로컬라이제이션 수집 설정이 없으니 패키징하면 반드시 영문이 된다. 에디터에서 잘 되는 걸 보고 넘어갔다가 패키징 때 깨지는 종류다.
그래서 화면 문구는 enum 메타데이터가 아니라 DataAsset 에 FText 로 따로 둔다. 에디터 라벨(UMETA)과 화면 문구는 원래 목적이 다르기도 하다. 드롭다운의 "없음" 과 화면의 "도구를 들고있지 않습니다." 는 같을 이유가 없다.
새로 만들려다 보니, 이 프로젝트에 똑같은 구조가 이미 있었다. CWWearableCatalog — 착용물을 DataAsset 으로 관리하고 IsDataValid 로 검증한다. 그대로 따라갔다.
USTRUCT(BlueprintType)
struct CROSSWALK_API FCWToolUIData
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "CW|UI|Tool")
FText DisplayName;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "CW|UI|Tool")
TObjectPtr<UTexture2D> Icon = nullptr;
};
UCLASS(BlueprintType)
class CROSSWALK_API UCWToolUIDataAsset : public UPrimaryDataAsset
{
// ToolUIDataMap : TMap<ECWToolType, FCWToolUIData>
// FindToolUIData(ToolType, OutData) — 없으면 로그 남기고 false
// IsDataValid — 저장 시 누락 항목·빈 이름 검출
};
코드 리뷰에서 하나 배웠다. 아이콘을 FSlateBrush 로 두려다가 TObjectPtr<UTexture2D> 로 바꿨다.
FSlateBrush ImageSize 가 0,0 이면 아이콘이 화면에 안 나온다
텍스처만 꽂고 크기를 안 채우기 쉽다. 에러도 경고도 없다
UTexture2D 크기는 Image 위젯이 정하고, 데이터는 "어떤 그림인지"만 갖는다
역할이 갈린다. 크기는 레이아웃의 몫, 데이터는 내용만. 나중에 머티리얼이나 아틀라스가 필요하면 그때 브러시로 바꾸면 된다.
ECWToolType 은 내 파일이 아니다(다른 팀원 담당). 그래서 도구가 추가되는 건 그 사람 손이다.
enum 에 Shovel 추가
→ DataAsset 에 Shovel 항목이 없음
→ 이름·아이콘이 빈칸
→ 컴파일 에러도 경고도 없음
switch 라면 컴파일이 깨져서 바로 알았을 텐데, TMap 조회는 조용히 넘어간다. 이게 DataAsset 의 유일한 약점이다. 그걸 IsDataValid 로 메운다.
#if WITH_EDITOR
EDataValidationResult UCWToolUIDataAsset::IsDataValid(
FDataValidationContext& Context
) const
{
const UEnum* ToolEnum = StaticEnum<ECWToolType>();
const int64 MaxEnumValue = ToolEnum->GetMaxEnumValue();
for (int32 Index = 0; Index < ToolEnum->NumEnums(); ++Index)
{
const int64 EnumValue = ToolEnum->GetValueByIndex(Index);
if (EnumValue == MaxEnumValue) continue; // _MAX 제외
const ECWToolType ToolType = static_cast<ECWToolType>(EnumValue);
const FCWToolUIData* Data = ToolUIDataMap.Find(ToolType);
if (Data == nullptr) // 항목 누락
{
Context.AddError(...);
continue;
}
if (Data->DisplayName.IsEmpty()) // 이름 비어 있음
{
Context.AddError(...);
}
}
return Result;
}
#endif
NumEnums() - 1 로 마지막 항목을 자르는 대신 GetMaxEnumValue() 로 실제 최대값을 제외했다. 마지막이 항상 _MAX 라는 보장이 없어서다. enum 에 UMETA(Hidden) 이 섞이거나 순서가 바뀌면 -1 방식은 깨진다.
이게 이번에 제일 신경 쓴 부분이다. 안전장치를 넣고 동작 확인을 안 하면 껍데기다.
에셋을 만들 때, 일부러 세 가지를 깨뜨려 봤다.
1 항목 3개만 넣고 저장 → 나머지 3개 "항목이 없습니다" 뜨는지
2 한 항목 이름을 비우고 저장 → "DisplayName이 비어 있습니다" 뜨는지
3 둘 다 확인되면 → 6개 다 채우고 통과
세 개가 다 떠야 두 종류 검사가 다 동작하는 거다. 하나만 테스트하고 넘어가면 다른 하나는 안 잡힐 수도 있다. 실제로 검증기가 6개를 다 잡아내는 걸 보고 나서야 채웠다.
한 가지 함정도 있었다. 이름을 채울 때 DisplayName 에 enum 의 영문 이름을 그대로 복사했다. Broom, WhiteRoller. 검증은 통과했다 — 비어 있지 않으니까. 하지만 화면에는 영문이 나온다. IsDataValid 는 "비었는지"만 보지 "한글인지"는 못 본다. 내용이 맞는지는 사람이 봐야 한다. 다 한글로 고쳤다.
장비 패널은 WorkerComponent 에서 현재 도구를 읽는다. 그 컴포넌트를 어떻게 얻느냐가 문제였다.
이전 코드(체온 게이지 버그를 잡으면서 배운 것)의 교훈을 그대로 적용했다.
Construct 에서 캐싱 HUD 생성 시 Pawn 이 없으면 영구히 null
리스폰·맵 이동 후 이전 컴포넌트를 계속 읽음
매 프레임 다시 조회 Get Owning Player Pawn → WorkerComponent
느려 보이지만 위젯 하나짜리라 무료에 가깝다
GetWorkerComponent 함수를 만들어 매번 Pawn 에서 다시 찾게 했다. 그리고 값이 바뀔 때만 UI 를 갱신한다.
Event Tick
→ GetWorkerComponent → Is Valid
Valid → GetHeldTool → CurrentTool
(초기화 안 됨 OR CurrentTool != LastTool) 이면
→ RefreshToolDisplay(CurrentTool)
Not Valid → ClearToolDisplay
여기서 함정을 하나 만났다. LastToolType 만 캐싱하면, 게임 시작 시 실제 도구도 None 이면 최초 갱신을 건너뛴다.
LastToolType 기본값 = None
실제 첫 도구 = None
→ 값만 비교하면 같다 → "도구를 들고있지 않습니다." 가 안 그려짐
그래서 bHasInitializedToolDisplay 라는 불리언을 하나 더 뒀다. 조건이 초기화 안 됨 OR 값이 다름 이라, 한 번은 무조건 그린다. Pawn 이 사라졌다 돌아올 때도 이 플래그를 false 로 내려서, 같은 도구여도 다시 그리게 했다.
새 흐름이 동작하는 걸 확인한 뒤, 기존 것을 지웠다. 여기서 규칙 하나를 지켰다. 하나 지우고 컴파일, 하나 지우고 컴파일.
GetToolNameText 바인딩 제거 → 컴파일
GetToolNameText 함수 삭제 → 컴파일
SetEquipmentData 삭제 → 컴파일
ToolBrush / ToolName 변수 삭제 → 컴파일
한 번에 다 지우면 뭐가 깨졌는지 안 보인다. 하나씩 지우면, 컴파일 에러가 나는 순간 "방금 지운 게 아직 쓰이는 곳이 있다" 를 바로 안다.
그리고 삭제 전에 Find References 를 한 번씩 했다. 블루프린트는 호출하는 에셋이 로드 안 된 상태면, 현재 위젯 컴파일만으로 외부 참조가 안 보일 수 있다. 그래서 마지막에 WBP_InGameHUD(장비 패널을 품는 상위 위젯)를 열어 Refresh All Nodes 로 확인했다. SetEquipmentData 를 아무도 안 부르는 걸 grep 으로 미리 확인해 둔 덕에 안심하고 지웠다.
UMETA(DisplayName) 은 런타임 UI 문구로 쓰면 안 된다. 읽는 경로가 #if WITH_EDITOR 안이라 패키징에서 내부 이름으로 떨어진다. 화면 문구는 FText 로 따로 둔다.IsDataValid 로 저장 시점에 잡는다.GetMaxEnumValue() 로 최대 항목을 제외한다. NumEnums()-1 은 마지막이 _MAX 라는 가정이라 enum 구성이 바뀌면 깨진다.
| 팀프로젝트 17. 페인트 보급 HUD (0) | 2026.08.26 |
|---|---|
| 팀프로젝트 16. UI 페이지 정책 데이터 에셋화 (0) | 2026.08.26 |
| 팀프로젝트 14. 화면 동결과 공용 피드백 레이어 (0) | 2026.08.24 |
| 팀프로젝트 13. 체온 게이지와 해상도 표시 버그 (0) | 2026.08.21 |
| 팀프로젝트 12. 환경설정 화면 구현 (0) | 2026.08.19 |