1인 개발/기능 구현

Bit Flag Enum으로 스킬 태그 만들기

밀하우스 마나스톰 2021. 7. 7. 11:03

 

Path Of Exile 게임의 스킬 툴팁을 보면 스킬마다 여러 가지 태그를 가지고 있는 것을 볼 수 있다.

 

스킬 태그 목록을 이용하여 여러가지 작업을 할 수 있다.

 

예를 들면 '카오스 스킬 사용시 잃은 체력 1% 회복'이라는 옵션을 가진 아이템을 플레이어가 착용했다면

 

어떤 스킬을 사용할 때마다, 해당 스킬의 태그 목록에 '카오스'가 있는지 확인하고 체력을 회복시키는 로직을 쉽게 구현할 수 있을 것이다.

 

아니면 '저주 면역'이라는 옵션을 가진 몬스터에게 플레이어가 어떤 스킬을 사용하면

 

해당 스킬의 태그 목록에 '저주'가 있는지 확인해서 해당 스킬이 몬스터에게 통할지 안 통할 지를 정할 수 있다.

 

 

 

또 다른 예시로, 궁수의 전설의 스킬 툴팁을 보자.

 

어떤 태그들을 가지고 있는지 표시하고 있지 않지만, 만약 똑같이 구현한다고 하면

 

스피드 오라 스킬은 지속시간, 스텟증가, 쿨타임. 세 가지 태그를 가지고 있는 스킬로 정의할 수 있을 것이다.

 

그리고 스킬 정보 클래스에 value1(타겟 스텟), value2(증가량) 필드를 만들고 이 값들을 스킬마다 다르게 초기화하면 될 것 같다.

 

 

SkillInfo speedAura = new SkillInfo();
speedAura.SetTags(SkillTag.Duration | SkillTag.IncreaseStat | SkillTag.CoolTime);
speedAura.SetValues(2.0f, "Speed", 62.5f, 8.0f);

 

코드로 치면 이런 느낌이지 않을까 싶다.

 

value 값을 초기화하는 방법, 즉 SetValues 내부의 로직은 여러 가지 방법으로 구현할 수 있을 것이다.

 

예시와 같이 태그 목록과 SetValues로 부터 전달받은 파라미터들을 대조해서 스킬 정보의 멤버 변수들을 초기화하는 방법도 있다.

 

굳이 value1, value2 같은걸 만들지 않고서 바로 멤버 변수에 접근해서 초기화하는 방법도 있다.

 

speedAura.coolTime = 8.0f; 이런 식으로 직관적으로 접근이 가능하고, 쿨타임이 없는 스킬은 0으로 초기화하면 된다.

 

보통 스킬 정보는 DB에서 테이블 형식으로 읽기 때문에, 스킬마다 레코드의 모든 필드를 읽어서 초기화해야하는 장단점이 있다.

 

 


 

 

1. 일반적인 enum 클래스

 

public enum SkillTag
{
	Attack,			// 공격
	Spell,			// 주문
	Projectile,		// 투사체
	Movement,		// 이동
	Buff,			// 버프
	Duration,		// 지속시간
	Range,			// 효과범위
	Fire,			// 화염
	Cold,			// 냉기
	Chaos,			// 카오스
	Light,			// 빛
}

 

Path Of Exile의 스킬 태그들을 대충 따라해서 SkillTag라는 enum 클래스와 필드들을 만들었다.

 

 

SkillTag skillTag = SkillTag.Attack;

 

일반적인 enum 클래스로 선언된 변수는 하나의 태그만 가질 수 있다.

 

 

SkillTag skillTag = SkillTag.Spell + SkillTag.Cold;
SkillTag skillTag = SkillTag.Spell | SkillTag.Fire;

 

일반적인 enum 클래스를 이용해서 이런 식으로 두 개의 태그를 가지게 하는 것은 불가능하다.

 

enum 클래스는 정수 타입이다. 정수 타입에 key 값을 붙인 것이다.

 

첫 번째 줄은 에러가 나고, 두 번째 줄은 Spell(1)과 Fire(7)를 | 연산(OR)한 결괏값인 Cold(8)을 의미하게 된다.

 

enum 클래스를 마치 리스트 형태로 쓸 수 있으려면 조합 연산의 결과값은 항상 고유해야 한다.

 

조합의 결과값 8이 enum 클래스에 존재하지 않으면 된다.

 

 


 

 

2. enum 클래스 필드들의 값을 비트 단위로 변경

 

public enum SkillTag
{
	Attack		= 1,
	Spell		= 2,
	Projectile	= 4,
	Movement	= 8,
	Buff		= 16,
	Duration	= 32,
	Range		= 64,
	Fire		= 128,
	Cold		= 256,
	Chaos		= 512,
	Light		= 1024,
}

 

필드 값들을 0,1,2,3,4...가 아닌 비트 단위(2의 제곱)로 설정하면

 

그 어떤 필드 값들끼리 더하고 빼는 조합을 하더라도 enum 클래스에 이미 존재하는 필드와 동일한 값이 나오지 않는다.

 

이 enum 클래스는 '태그 집합'의 의미로 사용할 것이기 때문에, Spell 필드를 두 번 더한다던지 이런 조합은 하지 않는다는 전제는 깔린다.

 

 

public enum SkillTag
{
	Attack		= 1 << 0,
	Spell		= 1 << 1,
	Projectile	= 1 << 2,
	Movement	= 1 << 3,
	Buff		= 1 << 4,
	Duration	= 1 << 5,
	Range		= 1 << 6,
	Fire		= 1 << 7,
	Cold		= 1 << 8,
	Chaos		= 1 << 9,
	Light		= 1 << 10,
}

 

그리고 수작업으로 2의 제곱을 계산해서 값을 초기화하는 건 가독성이 떨어지고 오타의 위험이 있으므로

 

시프트 연산자 << 를 활용해서 값을 초기화해주는 게 좋다.

 

피연산자 10진수를 2진수로 변환한 뒤 지정한 비트 수만큼 왼쪽 또는 오른쪽으로 이동시킨 후 다시 10진수 변환하는 원리다.

 

 


 

 

3. 활용하기

 

먼저 조합 연산의 결과값이 무엇을 의미하는지를 확인할 수 있어야 한다.

 

보통은 enum 클래스의 필드에 .ToString()을 하고, 그 필드의 key값(string)을 반환받아서 알 수 있다.

 

 

 

문제는 위와 같이 연산의 결과값 130이 SkillTag에 존재하지 않기 때문에 "130"이라는 값이 나온다.

 

이 130이 무엇을 의미하는지 알 방법이 없고, FlagsAttribute API를 사용해야 한다.

 

(공식 레퍼런스 : https://docs.microsoft.com/ko-kr/dotnet/api/system.flagsattribute?view=net-5.0)

 

 

[Flags]
public enum SkillTag
{
	Attack		= 1 << 0,
	Spell		= 1 << 1,
	Projectile	= 1 << 2,
	Movement	= 1 << 3,
	Buff		= 1 << 4,
	Duration	= 1 << 5,
	Range		= 1 << 6,
	Fire		= 1 << 7,
	Cold		= 1 << 8,
	Chaos		= 1 << 9,
	Light		= 1 << 10,
}

 

사용 방법은 enum 클래스 앞에 [Flags] 애트리뷰트를 붙이면 된다.

 

이제 해당 enum 클래스는 플래그(태그)의 집합으로 처리된다.

 

 

 

다시 ToString()의 반환 값을 보면 어떤 태그들의 조합인지 알 수 있다.

 

 

[Flags]
public enum SkillTag
{
	None		= 0,
	Attack		= 1 << 0,
	Spell		= 1 << 1,
	Projectile	= 1 << 2,
	Movement	= 1 << 3,
	Buff		= 1 << 4,
	Duration	= 1 << 5,
	Range		= 1 << 6,
	Fire		= 1 << 7,
	Cold		= 1 << 8,
	Chaos		= 1 << 9,
	Light		= 1 << 10,
	All		= int.MaxValue
}

 

본격적으로 활용하기 위해서 None(어떤 태그도 갖지 않음)과 All(모든 태그를 가짐) 필드를 양 끝에 추가해준다.

 

 

 

■ 모든 태그 제거

 

skillTag = SkillTag.None;

 

 

■ 모든 태그 추가

 

skillTag = SkillTag.All;

 

 

■ 특정 태그들의 조합으로 초기화

 

skillTag = SkillTag.Attack | SkillTag.Range;

 

 

■ 특정 태그들을 제외한 모든 태그 조합으로 초기화

 

skillTag = SkillTag.Spell ^ SkillTag.Projectile ^ SkillTag.Movement;

 

 

■ 기존 태그 조합에 새로운 태그 추가

 

skillTag |= SkillTag.Chaos;

 

 

■ 기존 태그 조합에 특정 태그 제거

 

skillTag &= ~SkillTag.Chaos;

 

 

■ 반전 (태그가 있으면 제거하고 없으면 추가) 

 

skillTag ^= SkillTag.Attack;

 

 

■ 특정 태그가 있는지 검색

 

if ((skillTag & SkillTag.Projectile) != 0) { }   // .NET 4.0 미만 버전
if (skillTag.HasFlag(SkillTag.Projectile)) { }   // .NET 4.0 이상 버전

 

 

■ 모든 필드를 배열로 가져오기

 

Array array = Enum.GetValues(typeof(SkillTag));

 

 

자주 쓰이는 기능들은 위와 같이 사용하면 된다.

 

이 외에도 enum 클래스와 FlagsAttribute API가 제공하는 함수들이 더 있다.

 

위에서 링크해놓은 레퍼런스 사이트에서 확인할 수 있으며, 몇몇 기능은 .NET 버전에 따라 사용할 수 없다.