[Unity/C#] | 포션 시스템

2024. 9. 27. 08:15·개발/Unity

개요

반갑습니다.

오랜만에 개발일지로 찾아뵙습니다.

 

...하지만 개발일지라기보단 이미 개발된걸 되짚어보는 글에 가깝습니다.

 

각설하고, 시작하죠.

 

 


 

 

포션을 왜?

한동안 프로젝트 활동이 없던 제가 왜 갑자기 개뜬금없이 포션 시스템을 만드느냐 하면?

복잡한 사정이 겹치고 겹쳐 현재 결선에 올라가있는 STA+C팀에 개발자로서 참여하게 됐습니다.

 

그래서 그 게임의 메인 기믹인 포션을 제가 맡아 개발하기로 했죠.

 

대충 이런 포션들입니다.

식물을 성장 시키는 '성장 포션'

 

적을 석화 시키는 '석화 포션'

 

 

원래 버프 디버프 시스템도 작성했지만 글이 너무 길어질것 같아 일단 떼어놓고,

이제 이걸 어떻게 만들었는지 하나하나 풀어보죠.

 

 


 

 

아이디어 정리

기본적으로 포션을 각자의 이펙트마다 하나하나 클래스를 만들어 관리하는건 비효율적이라고 생각했습니다.

(ex : GrowingPotion, PetrificationPotion)

...지금와선 없는 기획이긴 하지만 기획상으로 재료를 잘 섞으면 한번에 두개의 효과가 튀어나오는 포션이 생길 수도 있었죠.

 

그래서 모든 포션은 이펙트 발사대로써 작용합니다.

 

포션은 이펙트들을 배열로 받아 한번에 여러개의 이펙트를 중첩해서 줄 수 있도록 만들어집니다.

 

그러면 Potion은 Effect의 효과들을 실행만 시켜주면 되니 Effect 클래스만 만들어 따로 구현해주면 되겠죠.

한번 만들어보죠.

 

 


 

 

구현

IAffectable

Potion을 구현하기 전에 Effect를 받는 대상을 정의하기 위해 인터페이스를 작성해줍니다.

기존엔 모든 Enemy, Player의 기본이되는 Entity 클래스를 대상으로 정해줬지만 성장 포션을 만들기 많이 어려워지더라구요.

그래서 인터페이스로 분리해주겠습니다.

public interface IAffectable
{
    public void ApplyEffect();
}

 

간단하게... IAffectable(효과를 받을 수 있는)으로 작명해줬습니다.

 

 

Potion & Effect

이제 Potion 클래스를 만들어볼까요?

public abstract class Potion : MonoBehaviour
{
    public List<Effect> effects;
    public Entity owner;
    public int level;
}

그냥 대충 만들어주면 됩니다.

포션의 종류는 던지는 것, 마시는 것. 총 2개가 있기 때문에 추상 클래스로 만들어줍니다.

 

 

그리고 Effect를 만들어줍시다.

public abstract class Effect
{
    protected List<IAffectable> _affectedTargets;
    protected int _level;
    protected Potion _potion;

    public void Initialize(Potion potion, int level)
    {
        _potion = potion;
        _level = level;
    }

    public void SetAffectedTargets(List<IAffectable> targets)
    {
        _affectedTargets = targets;
    }

    public abstract void ApplyEffect();
}

 

일단 Effect를 받는 타겟들과 레벨, 포션 주체를 갖고 있게 해주고 타겟들을 외부에서 받아올 수 있도록 해줍니다.

타겟들은 Potion에서 체킹해서 각 Effect들에게 넣어주겠죠.

 

그리고 마지막으로 가장 중요한 구현부를 추상함수로써 놓아줍니다.

 

EffectManager

Potion들이 Effect들을 적절하게 가져올 수 있도록 EffectManager를 만들어줄겁니다.

왜 이렇게 하냐면 모든 Potion들은 자신만의 Effect 클래스 인스턴스가 필요하거든요.

만약 모든 포션이 같은 Effect 인스턴스를 공유하게되면 개판이나겠죠.

 

그러다보니 EffectManager는 원하는 Effect 클래스를 만들어서 뱉어주는 역할을 합니다.

이것 외에는 하지 않기 때문에 static 클래스로 놓아줍니다.

public static class EffectManager

 

 

그 뒤에 클래스를 더욱 용이하게 가져올 수 도록 Enum과 Dictionary를 사용해줍니다.

항상 생각하는데 이렇게 구조짜면 만족스럽습니다.

 

public enum EffectTypeEnum
{
    Damage, //데미지
    Petrification, //석화
    Growing, //성장
    Heal, //회복
    PoorRecovery, //힐 감소
    Stun, //기절
    Slowdown, //이속 감소
    Fragile, //받는 데미지 증가
    DotDeal, //도트딜
...

너무 길어서 생략하겠습니다. 대충 enum을 만들었어요.

 

이제 Dictionary로 묶어줍니다.

private static readonly Dictionary<EffectTypeEnum, Func<Effect>> _effectDictionary;

 

클래스의 인스턴스를 만들어서 전달하기 위해서 Action이 아닌 반환값이 존재하는 Func<T>를 사용하여줍니다.

 

그리고... 이제 노가다죠.

대리자다보니 Reflection을 사용할 수 없습니다.

 

...음 Func대신 Type으로 묶는다면 Reflection을 사용할 수는 있지만, Activator의 CreateInstace보다 일반 생성자가 빨라서 이런 방법을 채택했습니다.

유연성은 좋지 않지만 성능이라도 챙겨야죠. Reflection이 런타임 도중에 돌아가는건 자제하는게 좋습니다.

static EffectManager()
{
    _effectDictionary = new Dictionary<EffectTypeEnum, Func<Effect>>
    {
        { EffectTypeEnum.Damage, () => new DamageEffect() },
        { EffectTypeEnum.Petrification, () => new PetrificationEffect() },
        { EffectTypeEnum.Growing, () => new GrowingEffect() },
        { EffectTypeEnum.Heal, () => new HealEffect() },
        { EffectTypeEnum.Floating, () => new FloatingEffect() },
        { EffectTypeEnum.Weak, () => new WeakEffect() },
        { EffectTypeEnum.Slowdown, () => new SlowdownEffect() },
        { EffectTypeEnum.PoorRecovery, () => new PoorRecoveryEffect() },
        { EffectTypeEnum.Speed, () => new SpeedEffect() },
        { EffectTypeEnum.Resistance, () => new ResistanceEffect() },
        { EffectTypeEnum.NatureSync, () => new NatureSyncEffect() },
        { EffectTypeEnum.Strength, () => new StrengthEffect() },
        { EffectTypeEnum.Fragile, () => new FragileEffect() },
        { EffectTypeEnum.Spike, () => new SpikeEffect() },
        { EffectTypeEnum.HornSpike, () => new HornSpikeEffect() },
        { EffectTypeEnum.SpikeShield, () => new SpikeShieldEffect() },
    };
}

 

 

길죠? 하지만 이렇게 하면 편하긴합니다.

 

Effect를 가져오기 위해선 이렇게 하면 되죠.

public static Effect GetEffect(EffectTypeEnum effectEnum)
{
    if (_effectDictionary.TryGetValue(effectEnum, out Func<Effect> effect))
    {
        return effect.Invoke();
    }
    return null;
}

혹시 모를 예외처리는 반필수입니다.

 

 

다시 Potion

이제 이 Effect들을 생성해주기 위해 Potion을 만져줘야합니다.

인스펙터에서 만지면 훨씬 편하기 때문에 일단 인스펙터에서 이쁘게 볼 수 있게 구조체를 만들어주겠습니다.

[Serializable]
public struct PotionInfo
{
    public EffectTypeEnum effectEnum;
    public int level;
}

 

이 구조체를 인스펙터에서 보면 이런느낌입니다.

 

 

아무튼 이걸 이제 배열로 만들어 포션 클래스의 멤버 변수로 넣어줍니다.

[SerializeField]
private PotionInfo[] _potionInfos;

 

그러면 이제 이런식으로 Potion의 Effect를 만들어줄 수 있습니다.

for (int i = 0; i < _potionInfos.Length; i++)
{
    Effect effect = EffectManager.GetEffect(_potionInfos[i].effectEnum);
    effect.Initialize(this, _potionInfos[i].level);
    effects.Add(effect);
}

 

 


 

 

뭐라도 만들어보기

구조는 전부 작성되었습니다. 이제 테스트로 투척 포션이랑 성장 이펙 한번 만들어보겠습니다.

 

일단 투척 포션은 벽이나 적과 만나면 근처 원 범위에 있는 이펙트 적용 가능 대상을 받아와줘서 Effect 클래스에 모두 넘겨주고 실행시켜주면 됩니다.

public override void UsePotion()
{
    int count = Physics2D.OverlapCircleNonAlloc(transform.position, range, _colliders, _whatIsEnemy);
    List<IAffectable> list = new List<IAffectable>();
    if(count > 0)
    {
        for (int i = 0; i < count; i++)
        {
            if (_colliders[i].TryGetComponent(out IAffectable entity))
            {
                list.Add(entity);
                
            }
        }
    }
    foreach (Effect effect in effects)
    {
        effect.SetAffectedTargets(list);
        effect.ApplyEffect();
    }
}

...쉽죠?

 

굳이 쓸데없는 부분은 긁어오지 않겠습니다.

 

 

이제 성장포션을 만들건데 얘도 쉬워요. 일단 클래스를 작성해야겠죠.

public class GrowingEffect : Effect

 

 

그리고 미리 만들어둔 성장 할 오브젝트들만 가져와서 실행시키면 되죠.

public override void ApplyEffect()
{
    foreach (var target in _affectedTargets)
    {
        if (target is GrowingGrass)
        {
            target.ApplyEffect();
        }
        else if(target is GrowingBush)
        {
            target.ApplyEffect();
        }
        else if(target is BlockVine)
        {
            target.ApplyEffect();
        }
    }
}

 

쉽죠?

 

 


 

 

무엇이 문제일까

STA+C 대회는 약 6달정도 진행이 됩니다. 하지만 저희 팀은 특이한 상황이 벌어져서 사실상 개발을 1달밖에 진행하지 못해서 그런지 구조가 괜찮아보이지만 나사가 몇개 빠져있습니다.

 

일단 당장 보이는 이슈.

Effect의 IAffectable List는 일부 포션이 아니면 '절대' 사용하지 않습니다.

 

이를 수정하기 위해선 Effect엔 ApplyEffect 함수 정도만 남겨주고 Target을 받아야 되는 경우엔 TargetEffect라는 Effect를 부모 클래스로 두는 또 다른 추상 클래스로 만들어서 해결할 수 있을법 합니다.

public class TargetEffect : Effect
{
	protected List<IAffectable> _affectableList;
    
    public void SetAffectableList(List<IAffectable> list)
    	=> _affectableList = list;
}

 

이렇게 말이죠.

 

 

다른 부분을 조금 볼까요.

일단 이 IAffectable 인터페이스의 사용처가 그냥 그룹화가 전부에요.

IAffectable을 구현하는 클래스중 일부가 ApplyEffect를 사용하지 않는다는 뜻이죠.

구조적으로 불필요합니다.

 

어떻게 해결할 수 있을까

당장 생각나는건 인터페이스를 분리하는겁니다.

ISP(Interface Segregation Principle, 인터페이스 분리 원칙)라고 하죠.

 

IAffectable을 명확한 구현체가 없는 그룹화만을 위한 인터페이스로 만듭니다.

public interface IAffectable { }

 

그리고 이런식으로 필요한 것들만 상속받아 따로 만들어줍니다.

public interface IGrowable : IAffectable
{
    void Grow();
}

 

이러면 해결이 되고 가독성도 올라가고 저 위의 성장 이펙트 타입체크도 다음과 같이 축약가능 해집니다.

public override void ApplyEffect()
{
    foreach (var target in _affectedTargets)
    {
        if (target is IGrowable growable)
        {
            growable.Grow();
        }
    }
}

 

 

...이러면 좀 볼만하지 않을까요?

 


 

 

마치는 말

블로그를 하도 안쓰다보니 양심이 좀 찔려오더군요. 이런식으로라도 리프레쉬를 좀 해야겠어요.

조만간 동아리 프로젝트가 마무리되면 이제 남은 프로젝트가 졸업작품 단 하나가 됩니다.

잘 풀렸으면 좋겠네요.

 

...아 STA+C은 어제 제출했습니다. 게임도 itch.io에 출시 돼있죠.

https://variableoccurrer.itch.io/the-alchemists

 

The Alchemist's by VariableOccurrer

We invite you to the world of battle with various potions. [The Alchemist's] is a rifleman alchemy activity that hunts with potions. Gameplay This game is a platformer adventure game. that travel the world, collects materials, and makes various potions wit

variableoccurrer.itch.io

이 곳에서 다운할 수 있습니다.

'개발 > Unity' 카테고리의 다른 글

[Unity/C#] 버프, 디버프 시스템입니다. 근데... 비트 마스킹을 곁들인.  (0) 2024.11.07
[Unity/C#] | Animation Event 와 비트 마스킹  (3) 2024.08.05
'개발/Unity' 카테고리의 다른 글
  • [Unity/C#] 버프, 디버프 시스템입니다. 근데... 비트 마스킹을 곁들인.
  • [Unity/C#] | Animation Event 와 비트 마스킹
SundG0162
SundG0162
블로그 프로필은 머핀입니다.
  • SundG0162
    게임개발고수가될거야
    SundG0162
  • 전체
    오늘
    어제
    • 분류 전체보기 (77)
      • 주저리 (2)
        • 잡담 (1)
        • 장현우 (0)
        • 회고록 (1)
      • 개발 (50)
        • CITADEL : 성채 (1)
        • HEXABEAT (1)
        • FRACTiLE (6)
        • UNNAMED (9)
        • Default Defense (10)
        • T-Engine (1)
        • Project EW (0)
        • 졸업작품 (0)
        • Unity (3)
        • C# (4)
        • C++ (13)
        • WinAPI (1)
        • 그 외 (0)
      • 알고리즘 (13)
        • C# (1)
        • C++ (12)
      • 자료구조 (2)
        • C++ (1)
        • C# (0)
        • 공용 (1)
      • 기타 (10)
        • 아트 (6)
        • AI (2)
        • 수학 (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    AI
    코딩트리조별과제
    오블완
    C#
    생성형ai
    코딩
    LLM
    코드트리
    코딩테스트
    티스토리챌린지
    유니티
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
SundG0162
[Unity/C#] | 포션 시스템
상단으로

티스토리툴바