언리얼 치트의 경우는 컴파일러가 직접 호출하는게 아니라 문자열을 런타임에 파싱해서 파라미터를 조립하는 방식
치트매니저에 함수들은 UFUNCTION(exec) 라는 매크로가 붙게되는데 이 경우 UHT 가 ProcessConsoleExec 함수를 통해 exec 함수를 탐색한다.
그리고 CallFunctionByNameWithArguments 함수에서 파라미터를 파싱하게 된다.
또한 내부에서는 FindFunction 이라는 함수를 통해 함수포인터를 실행하는 방식
UClass 의 이런 멤버변수가 있다.
TMap<FName, TObjectPtr<UFunction>> FuncMap;
여기에 <함수이름, 함수 포인터> 를 저장하게 되는데 콘솔 명령어를 입력하면 해당 함수포인터를 실행하는 방식이다.
컴파일러 직접 호출
- 함수 이름, 파라미터 타입을 컴파일 타임에 알아야 함
- 즉 "Slomo" 라는 문자열을 받아서 호출하는 게 불가능
리플렉션 호출
- 런타임에 문자열만 있으면 함수 찾아서 호출 가능
- 콘솔에서 문자열 입력받아서 실행하는 치트가 가능해짐
에디터에서 매개변수가 기본값으로 지정되어있을때 잘 실행되었던게 패키징에서는 매개변수 생략하면 동작하지않거나 아예 이상한 값으로 가는 문제가 있었다.
UFUNCTION(exec)
void SpawnTrainingBot(int32 bHero = 1, int32 InLevel = 1, uint8 TeamID = 100, float Distance = 500.0f);
SpawnTrainingBot 1 -1 → 안불림
SpawnTrainingBot 1 -1 100 → 불림
SpawnTrainingBot 1 -1 100 500 → 불림
UFUNCTION(Exec)
void ApplyAbnormalGameplayEffectToSelfByKey(FName Key, float duration = 0.f, float value = 0.f );
ApplyAbnormalGameplayEffectToSelfByKey Stun → 불림
UFUNCTION(Exec)
void SetSkillLevel(int32 SkillNumber, int32 Level = 1);
SetSkillLevel 5 → 안불림

CallFunctionByNameWithArguments 함수 내부이다.
Strcmp 함수 = 두 문자열이 같은지 판별하는 함수
ArgStr (치트 함수 파라미터) 가 비어있으면 기본 값 메타데이터 (CPP_Default_Level) 을 사용하는데 이 경로가 Editor 에서만 동작하는것
그리고 패키징에선 아래 if문 블록에서 ImportText_InContainer 가 들어가게 되고 타입마다 다른 구조체의 함수를 타게된다.
const TCHAR* FNumericProperty::ImportText_Internal(
const TCHAR* Buffer,
void* ContainerOrPropertyPtr,
EPropertyPointerType PropertyPointerType,
UObject* Parent,
int32 PortFlags,
FOutputDevice* ErrorText) const
{
if (Buffer != NULL)
{
const TCHAR* Start = Buffer;
if (IsInteger())
{
if (FChar::IsAlpha(*Buffer))
{
int64 EnumValue = UEnum::ParseEnum(Buffer);
if (EnumValue != INDEX_NONE)
{
if (PropertyPointerType == EPropertyPointerType::Container && HasSetter())
{
SetValue_InContainer(ContainerOrPropertyPtr, &EnumValue);
}
else
{
SetIntPropertyValue(PointerToValuePtr(ContainerOrPropertyPtr, PropertyPointerType), EnumValue);
}
return Buffer;
}
else
{
return NULL;
}
}
else
{
if (!FCString::Strnicmp(Start, TEXT("0x"), 2))
{
Buffer += 2;
while (Buffer && (FParse::HexDigit(*Buffer) != 0 || *Buffer == TCHAR('0')))
{
Buffer++;
}
}
else
{
while (Buffer && (*Buffer == TCHAR('-') || *Buffer == TCHAR('+')))
{
Buffer++;
}
while (Buffer && FChar::IsDigit(*Buffer))
{
Buffer++;
}
}
if (Start == Buffer)
{
// import failure
return NULL;
}
}
}
else
{
check(IsFloatingPoint());
// floating point
while (*Buffer == TCHAR('+') || *Buffer == TCHAR('-') || *Buffer == TCHAR('.')
|| (*Buffer >= TCHAR('0') && *Buffer <= TCHAR('9')))
{
Buffer++;
}
if (*Buffer == TCHAR('f') || *Buffer == TCHAR('F'))
{
Buffer++;
}
}
if (PropertyPointerType == EPropertyPointerType::Container && HasSetter())
{
SetNumericPropertyValueFromString_InContainer(ContainerOrPropertyPtr, Start);
}
else
{
SetNumericPropertyValueFromString(PointerToValuePtr(ContainerOrPropertyPtr, PropertyPointerType), Start);
}
}
return Buffer;
}
ImportText_InContainer -> ImportText_Internal -> FNumericProperty::ImportText_Internal 내부에서 정수와 소수의 분기점을 타게되는데
정수의 경우 Null 을 반환하고
소수의 경우 0 을 반환하기때문에
치트의 정수 매개변수를 생략하였을땐은 함수 자체를 파싱못하는것이고, 소수의 매개변수를 생략하였을때는 0 으로 들어가게된다.
패키징에서 안되면 에디터에서도 안되게끔 하는게 맞지않나 싶었다.
'언리얼' 카테고리의 다른 글
| SetViewTarget (0) | 2026.03.12 |
|---|---|
| Zen Storage (0) | 2025.12.31 |
| DerivedDataBackendGraph (0) | 2025.12.24 |
| Deproject 위젯 -> 월드 (1) | 2025.11.18 |
| World Widget Depth Test (0) | 2025.09.24 |