KoreanFoodie's Study

[언리얼] ENUM_CLASS_FLAGS 사용하기 (언리얼 플래그 사용법) 본문

Game Dev/Unreal C++ : Dev Log

[언리얼] ENUM_CLASS_FLAGS 사용하기 (언리얼 플래그 사용법)

GoldGiver 2023. 3. 14. 21:29

ENUM_CLASS_FLAGS 사용하기 (언리얼 플래그 사용법)

핵심 :

1. 여러 조건을 동시에 가질 수 있는 상황을 다를 때는, Enum Class 의 값을 Shift 연산자를 활용해 Bit Flag 처럼 사용하는 것이 좋다.
2. EnumClassFlags 파일에 정의된 템플릿 함수들(e.g. EnumHasAnyFlags)을 활용하면, Enum Class 에 정의된 플래그들을 이용해 케이스들의 중첩을 효율적으로 체크할 수 있다.
3. 일반적으로 Enum Class 내의 값들을 Flag 로 표현하는 경우는 UI 나 환경설정 세팅 등이 있다.

일반적으로 Enum Class 는 같은 주제 내에서 여러 조건들을 다룰 때 사용한다.

그런데 만일 여러 복합적인 상황을 동시에 다뤄야 하는 케이스가 있다고 하면 어떨까?

예를 들자면, 전투 시스템을 만드는 과정에서 직업별로 취할 수 있는 행동의 조합을 다르게 만든다고 가정해 보자. 그럼 궁수는 활을 쏠 수 있고(ATTACK), 나무 위를 오를 수 있는(CLIMB_TREE) 등의 행동을 할 수 있지만, 워리어는 공격(ATTACK) 을 할 수는 있지만 나무 위를 오르지는 못하고, 대신 방어나 반격을 하는 기능을 넣을 수 있을 것이다.

다음 예시 코드를 보자 :

#include "CoreMinimal.h"

/* 
* 블루프린트에서 사용할 수 있게 만드려면 UENUM 을 붙여주면 된다.
* UseEnumValuesAsMaskValuesInEditor 를 이용해서 Enum 값을 에디터에서도 사용할 수 있다.
* E 는 Enum 을, Tp 는 TestProject 를 나타내는 접두어이다.
*/
UENUM(BlueprintType, meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true"))
enum class ETpCombatSystemFlags : uint32
{
	RUN = 1 << 1,
	JUMP = 1 << 2,
	BACK_JUMP = 1 << 3,
	ATTACK = 1 << 4,
	DEFEND = 1 << 5,
	AVENGE = 1 << 6,
	EVADE = 1 << 7,
	SNIPE = 1 << 8,
	CLIMB_TREE = 1 << 9,
	
	// 직업별 플래그 조합
	TRASH = 0,
	WARRIOR = RUN | JUMP | ATTACK | DEFEND | AVENGE,
	THEIF = RUN | JUMP | BACK_JUMP | ATTACK | EVADE,
	HUNTER = RUN | JUMP | ATTACK | SNIPE | CLIMB_TREE,
	

	GOD = 0xff,
}
ENUM_CLASS_FLAGS(ETpCombatSystemFlags);

위처럼 ETpCombatSystemFlags 에 각 직업별로 플래그를 조합했다. 그리고 TRASH 는 어떤 동작도 할 수 없는 녀석이고,  GOD 은 모든 것을 할 수 있는 녀석으로 설정했다. 

마지막에 ENUM_CLASS_FLAGS(ETpCombatSystemFlags) 는 EnumClassFlags.h 에 정의된 매크로로, 해당 Enum Class 의 Operator 를 재정의하는 역할을 한다.

// Defines all bitwise operators for enum classes so it can be (mostly) used as a regular flags enum
#define ENUM_CLASS_FLAGS(Enum) \
	inline           Enum& operator|=(Enum& Lhs, Enum Rhs) { return Lhs = (Enum)((__underlying_type(Enum))Lhs | (__underlying_type(Enum))Rhs); } \
	inline           Enum& operator&=(Enum& Lhs, Enum Rhs) { return Lhs = (Enum)((__underlying_type(Enum))Lhs & (__underlying_type(Enum))Rhs); } \
	inline           Enum& operator^=(Enum& Lhs, Enum Rhs) { return Lhs = (Enum)((__underlying_type(Enum))Lhs ^ (__underlying_type(Enum))Rhs); } \
	inline constexpr Enum  operator| (Enum  Lhs, Enum Rhs) { return (Enum)((__underlying_type(Enum))Lhs | (__underlying_type(Enum))Rhs); } \
	inline constexpr Enum  operator& (Enum  Lhs, Enum Rhs) { return (Enum)((__underlying_type(Enum))Lhs & (__underlying_type(Enum))Rhs); } \
	inline constexpr Enum  operator^ (Enum  Lhs, Enum Rhs) { return (Enum)((__underlying_type(Enum))Lhs ^ (__underlying_type(Enum))Rhs); } \
	inline constexpr bool  operator! (Enum  E)             { return !(__underlying_type(Enum))E; } \
	inline constexpr Enum  operator~ (Enum  E)             { return (Enum)~(__underlying_type(Enum))E; }

 

그리고 편하게 쓸 수 있게 정의된 클래스들은 다음과 같다 :

template<typename Enum>
constexpr bool EnumHasAllFlags(Enum Flags, Enum Contains)
{
	return ( ( ( __underlying_type(Enum) )Flags ) & ( __underlying_type(Enum) )Contains ) == ( ( __underlying_type(Enum) )Contains );
}

template<typename Enum>
constexpr bool EnumHasAnyFlags(Enum Flags, Enum Contains)
{
	return ( ( ( __underlying_type(Enum) )Flags ) & ( __underlying_type(Enum) )Contains ) != 0;
}

template<typename Enum>
void EnumAddFlags(Enum& Flags, Enum FlagsToAdd)
{
	Flags |= FlagsToAdd;
}

template<typename Enum>
void EnumRemoveFlags(Enum& Flags, Enum FlagsToRemove)
{
	Flags &= ~FlagsToRemove;
}

 

만약 특정 클래스의 직업이 특정 기능(예를 들어 DEFEND) 를 할 수 있는지 알고 싶다고 하면, 다음과 같이 쓰면 될 것이다 :

if (EnumHasAnyFlags(MyCombatClass, ETpCombatFlags::DEFEND))
{
    /* 방어와 관련된 어떤 동작 수행이 기대됨 ... */
}

 

만약 저주 아이템을 먹어서, 일시적으로 해당 클래스의 특정 기능(예를 들어 ATTACK)을 제거한다고 하면, 다음과 같을 것이다 :

if (EnumRemoveFlags(MyCombatClass, ETpCombatFlags::ATTACK))
{
    /* ... */
}

/* 저주 해제... */

if (EnumHasAnyFlags(MyCombatClass, ETpCombatFlags::ATTACK))
{
    /* ... */
}

 

만약 해당 Bitmask 기반 Enum Class 를 이용한 변수를 선언하고 싶으면, 다음과 같이 하면 된다 :

UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (Bitmask, BitmaskEnum = ETpCombatFlags))
int32 CombatFlags = 0;

 

함수는 다음처럼 만들면 된다(블루프린트에서 사용 가능하도록 설정할거라면 BlueprintCallable) :

UFUNCTION(BlueprintCallable)
bool CanDefend() const
{
	return CombatFlags & ETpCombatFlags::DEFEND;
}

 

만약 블루프린트 파라미터로 사용하고 싶으면(블루프린트에서 해당 함수를 조작하고 싶으면) :

// CombatTest.h
UFUNCTION(BlueprintCallable)
bool IsMatch(UPARAM(meta = (Bitmask, BitmaskEnum = EAnimDescriptorFlags)) int32 Bitmask);

// CombatTest.cpp
bool UCombatTest::IsMatch(int32 Bitmask)
{
	return Bitmask & CombatFlags == Bitmask;
}

위 함수는 다음과 같이 나온다!

 

참고로, Enum class 에 UPROPERTY 를 붙이고 싶을 경우, 다음과 같이 쓸 수도 있다 :

UPROPERTY()
TEnumAsByte<EnumName> VarName;

 

참고 : 블로그 1
Comments