개요
이번 글은 [Unity/C#] 포션 시스템의 후속 글입니다.
원활한 이해를 위해 글을 읽고 와주시면 감사하겠지만 귀찮다면 읽지 않아도 괜찮습니다.
아이디어 정리
일단 가장 저명한 게임인 마인크래프트를 예시로 들어보자면 버프와 디버프는 다음과 같이 중첩되어 있는 것이 일반적입니다.
이것을 관리하기 위해서 그냥 List 자료구조를 쓰는것도 괜찮지만 만약 특정 버프, 디버프가 들어있는지 판단을 해야한다면 그것을 판단하기 위해 탐색을 해야하기 때문에 O(n)의 시간 복잡도가 발생합니다.
물론 리스트가 길어봤자 얼마나 길겠냐마는 더 좋은 방법이 있는데 굳이 안 쓸 이유야 없잖아요?
그래서 비트 마스킹 기반으로 현재 버프, 디버프의 상태를 저장하고 구현부는 또 따로 돌려주기로 했습니다.
일단 각각의 상태이상들을 잘 관리하기 위한 클래스인 StatusEffect 기반으로 돌아갑니다.
이렇게 구현부를 따로 빼놓으면 보다 복잡한 버프, 디버프들을 구현하기 쉬워집니다.
그리고 이 각각의 StatusEffect들은 StatusEffectManager에서 모두 갖고있게됩니다.
그리고 이 StatusEffectManager는 또 Entity가 들어서 Update만 돌려주면 간단한 버프, 디버프 구조 끝입니다.
구현
StatusEffectEnum
일단 간단하게 Enum들을 만들어둡시다.
비트마스킹을 하기 위해 각 Enum의 값들은 2의 n제곱의 값을 갖습니다.
public enum StatusBuffEffectEnum
{
//IncreaseKnockback = 2, //넉백 증가
NatureSync = 16, //자연동화
Strength = 32, //힘
Resistance = 64, //받는 데미지 감소
Speed = 128, //속도 감소
SpikeShield = 256, //가시방패
}
public enum StatusDebuffEffectEnum
{
PoorRecovery = 1, //힐 감소
//Stun = 2, //기절*
Petrification = 4, //석화
Slowdown = 8, //이속 감소*
Fragile = 16, //받는 데미지 증가*
//DotDeal = 32, //도트딜
//Glow = 64, //발광
//Brainwash = 128, //아군 공격(세뇌)
Floating = 256, //부유
Weak = 512, //데미지 감소*
}
Buff랑 Debuff를 나눠놓은 이유는 기획상 디버프 시간 감소 포션이 있었기 때문입니다.
StatusEffect
그 후 StatusEffect(상태이상)라는 추상 클래스를 만들어줍니다.
public abstract class StatusEffect
{
private float _startTime;
protected float _cooltime;
protected Entity _target;
public int level;
private StatusBuffEffectEnum _buffEnum;
private StatusDebuffEffectEnum _debuffEnum;
public void Init(StatusBuffEffectEnum statusEnum)
{
_isBuffEffect = true;
_buffEnum = statusEnum;
}
public void Init(StatusDebuffEffectEnum statusEnum)
{
_isBuffEffect = false;
_debuffEnum = statusEnum;
}
public virtual void ApplyEffect(Entity target, float cooltime)
{
_target = target;
_startTime = Time.time;
_cooltime = cooltime;
}
public virtual void UpdateEffect() { }
public bool IsCompleted()
=>_startTime + _cooltime < Time.time;
}
잡다한것들 일단 스킵하고 간단하게 타입, 쿨타임, 레벨, 타겟 정도만 지정해줍니다.
StatusEffectManager
클래스 이름이 Manager다보니 싱글톤 같아 보이는데 싱글톤이 아니라 각각의 Entity에 붙는 상태이상 관리자입니다.
모든 StatusEffect를 돌려주고 쿨타임이 다 되면 지워주고... 기타등등 할일이 많습니다.
먼저 이런식으로 각각의 Dictionary들과 현재 적용되고 있는 StatusEffect 리스트를 만들어줍니다.
private Entity _owner;
private Dictionary<StatusBuffEffectEnum, StatusEffect> _statusBuffEffectDictionary;
private Dictionary<StatusDebuffEffectEnum, StatusEffect> _statusDebuffEffectDictionary;
private List<StatusEffect> _enableEffects;
그리고 생성자에서 모두 초기화 해줍니다.
public StatusEffectManager(Entity owner)
{
_owner = owner;
_enableEffects = new List<StatusEffect>();
_statusBuffEffectDictionary = new Dictionary<StatusBuffEffectEnum, StatusEffect>();
_statusDebuffEffectDictionary = new Dictionary<StatusDebuffEffectEnum, StatusEffect>();
foreach (StatusBuffEffectEnum effectEnum in Enum.GetValues(typeof(StatusBuffEffectEnum)))
{
string enumName = effectEnum.ToString();
Type t = Type.GetType($"{enumName}StatusEffect");
StatusEffect effect = Activator.CreateInstance(t) as StatusEffect;
effect.Init(effectEnum);
_statusBuffEffectDictionary.Add(effectEnum, effect);
}
.
.
.
길이상 조금 스킵하겠습니다.
그리고 각각의 StatusEffect들을 Update 해주고 StatusEffect를 추가하는 함수 또한 만들어주면 됩니다.
public void UpdateStatusEffects()
{
for (int i = _enableEffects.Count - 1; i >= 0; i--)
{
var effect = _enableEffects[i];
if (effect.IsCompleted())
{
effect.OnEnd();
_enableEffects.RemoveAt(i);
}
else
{
effect.UpdateEffect();
}
}
}
public StatusEffect AddStatusEffect(StatusBuffEffectEnum statusEffect, int level, float cooltime)
{
StatusEffect effect = _statusBuffEffectDictionary[statusEffect];
effect.SetInfo(level);
effect.ApplyEffect(_owner, cooltime);
_enableEffects.Add(effect);
return effect;
}
Update 함수가 리스트를 역순으로 탐색하는 이유는 foreach가 아닌 for문으로 List를 탐색도중에 List의 값이 바뀌어도 탐색을 잘 끝마치기 위해서입니다.
저렇게 안하면 IndexOutOfRangeException을 맛볼 수 있습니다.
그렇다고 foreach로 해도 마찬가지에요.
Entity
이제 Entity를 살짝 수정해봅시다.
그냥 StatusEffectManager 추가해주고...
private StatusEffectManager _statusEffectManager;
비트 마스킹용 변수 2개를 추가해줍니다.
protected int _buffStatusEffectBit = 0;
protected int _debuffStatusEffectBit = 0;
그리고 이런식으로 간단하게 해당 StatusEffect를 갖고 있는지 판단하는 코드를 추가하고,
public bool IsUnderStatusEffect(StatusBuffEffectEnum statusEffect)
=> (_buffStatusEffectBit & (int)statusEffect) != 0;
StatusEffect를 추가하는 코드 또한 만들어줍니다.
public StatusEffect ApplyStatusEffect(StatusBuffEffectEnum statusEffect, int level, float duration)
{
if (IsUnderStatusEffect(statusEffect)) return null;
_buffStatusEffectBit |= (int)statusEffect;
return _statusEffectManager.AddStatusEffect(statusEffect, level, duration);
}
그리고 마지막으로 Update만 돌려주면 완성입니다.
protected virtual void Update()
{
_statusEffectManager.UpdateStatusEffects();
}
왜 이렇게 짰을까
일단 StatusEffectManager라는 StatusEffect를 관리하는 클래스를 따로 만들었음에도 굳이굳이 비트 마스킹은 Entity에서 진행합니다. 대체 왜?
당장 StatusEffectManager를 변수로도 갖고 있으니 단순히 함수와 비트 마스킹을 StatusEffectManager로 빼기만 하면 괜찮았을텐데...
아무래도 저거 적을 당시에 조금 정신이 없었나봅니다.
마치는 말
간단하게 방치하고 있던 글 하나 마무리 했습니다.
글 퀄리티가 조금 구리네요. 읽어주셔서 감사합니다.
'개발 > Unity' 카테고리의 다른 글
[Unity/C#] | 포션 시스템 (5) | 2024.09.27 |
---|---|
[Unity/C#] | Animation Event 와 비트 마스킹 (3) | 2024.08.05 |