C#/디자인 패턴

[팩토리 패턴 + 프로토타입 패턴 활용] 일정 기간 동안 지속되는 효과 관리

Toa_ 2025. 4. 25. 20:12

 

 

 

로그라이크 게임에서는 일반적으로 다양한 랜덤 이벤트들이 존재하며, 이러한 랜덤 이벤트들은 스탯, 아이템, 전투 조건 등의 다양한 효과를 즉시, 혹은 정해진 기간 동안 실행해 준다.

이러한 랜덤 이벤트 효과들을 지금 제작하고 있는 게임에서 구현한다면, 여러 범위의 시스템과 연결되기 때문에 설계 단계에서 매우 높은 확장성을 고려한 구조가 필요하다고 생각했다.

 

 


 

 

 

처음에는 사전에 정해진 CSV형식의 데이터를 받아와서 등록 한 뒤, 적용 시점에서 주기에 맞게 구독, 그리고 일정 주기에 따라 여러 기능들을 한 번에 호출해 주는 방식을 action을 활용한다면 효율적으로 사용이 가능해 보였다.

 

하지만 효과가 즉시 적용되는 방식이 아닌 다음 전투까지, 이번 스테이지 동안, 모험이 끝날 때까지, 등의 주기일 경우 게임을 끌 때도 저장을 할 수 있어야 하는데 action을 통한 구독 형식의 경우에는 JSON 등의 데이터 저장으로 구독 정보의 저장이 불가능하다.

 

따라서 메서드를 action으로 등록하는 것이 아닌 action의 구독처럼 각 효과를 List처럼 만들어서 list의 효과들을 각각 재생해 주는 방식은 어떨까?라는 생각을 하였고.

이 방식이면 JSON 등으로 저장 또한 간단하게 구현할 수 있다 생각해, 위 구조에 맞춰 코드를 작성해 보았다.

 

 


 

 

 

먼저 이벤트 효과들은 공통으로 사용할 필드들과 필수로 가져야 할 메서드들이 존재하였고 따라서 추상 클래스 형식의 EventEffects를 상속받는 각 종류의 이벤트 효과들로 관리하고자 하였다.

 

public abstract class EventEffects
{
    public int index; // 인덱스
    public string text; // 효과 설명
    public int eventType; // 이벤트 타입 0: 스탯 관련, 1: 카드 관련, 2: 전투 발생 ...
    public int duration; // 지속 시간 0: 즉시, 1: 다음 전투, 2: 이번 스테이지. 3: 모험 전체   

    public abstract void Apply(); // 효과 적용 메서드
    public abstract void UnApply(); // 효과 해제 메서드 (카드 위주의 사용)

    public abstract EventEffects Clone();
}

 

 

이런 추상 클래스를 각각 능력치 효과, 카드 관련효과, 추가적인 시스템과 관련된 효과 등등으로 나누어 클래스를 만들어 주었다.

 

public class StatEventEffects : EventEffects
{
    // 필드
	//
    //
    
    public override void Apply()
    {
        // 필드 정보를 기반으로 효과 구현
    }
    public override void UnApply()
    {
    	// 직접 해재를 해줘야 하는 효과의 경우 여기서 해제
    }
    public override EventEffects Clone()
    {
        return new StatEventEffects
        {
            index = this.index,
            text = this.text,
            eventType = this.eventType,
            duration = this.duration,
            sophia = this.sophia,
            kyla = this.kyla,
            leon = this.leon,
            enemy = this.enemy,
            hp = this.hp,
            hpPercent = this.hpPercent,
            atk = this.atk,
            def = this.def
        };
    }
}

 

 

 

그리고 여기서 등장하는 Clone 은 프로토 타입 패턴으로 스스로의 깊은 복사를 하여 참조를 공유하지 않는 완전히 새로운 복사를 만들어 사용할 수 있도록 하였다. 자세한 내용은 아래에서 설명하겠다.

 

(여기서의 Clone은 사실상 얕은 복사지만 클래스의 모든 필드가 값 타입으로서 존재하기에 깊은 복사처럼 동작)

 

 


 

 

 

다음은 이렇게 만든 효과 적용의 클래스들의 필드값을 넣어주고, 관리 및 전역적인 접근을 가능하게 해 줄 EventEffectManager를 만들었다.

 

 

public class EventEffectManager : MonoSingleton<EventEffectManager>
{
    [Header("CSV Data path")]
    [SerializeField] string csvPath = "ExternalFiles/EventEffects.csv"; // CSV 파일 경로

    List<EventEffects> eventEffectList;
    
    // 액션 처럼 사용할 효과들 List
    List<EventEffects> untillNextCombat = new List<EventEffects>(); // 다음 전투까지 지속되는 효과 리스트
    List<EventEffects> untillNextStage = new List<EventEffects>(); // 다음 스테이지까지 지속되는 효과 리스트
    List<EventEffects> untillEndAdventure = new List<EventEffects>(); // 모험이 끝날 때까지 지속되는 효과 리스트
}

 

 

 

 

이러한 형식으로 만들어서  아래와 같이 CSV의 데이터를 파싱을 통해 나눈 데이터를 eventEffectList에 저장하도록 하였다.

이때 CSV 데이터의 eventType에 따라 데이터를 받아와 필요한 객체를 생성하여 반환하는, 팩토리 패턴을 사용했다.

 

 

 

void Awake()
{
    eventEffectList = LoadDatas(csvPath);
}

private List<EventEffects> LoadDatas(string csvPath) // CSV의 이벤트 효과 데이터를 기반으로 이벤트 효과의 리스트를 생성.
{
    string fullPath = $"{Application.dataPath}/Resources/{csvPath}";

    var eventEffectList = new List<EventEffects>();
    var eventEffectDatas = EventEffectCSVParser.Parse(fullPath);

    foreach (var data in eventEffectDatas)
    {
        switch (data.eventType)
        {
            case 0:
                var statEffect = new StatEventEffects
                {
                    index = data.index,
                    text = data.text,
                    eventType = data.eventType,
                    duration = data.duration,
                    sophia = data.sophia,
                    kyla = data.kyla,
                    leon = data.leon,
                    enemy = data.enemy,

                    hp = data.hp,
                    hpPercent = data.hpPercent,
                    atk = data.atk,
                    def = data.def
                };

                eventEffectList.Add(statEffect);
                break;

            case 1:
                var cardEventEffect = new CardEventEffects
                {
                    // 데이터 넣기
                };

                eventEffectList.Add(cardEventEffect);
                break;

            case 2:
                var enemyEventEffect = new EncounterEventEffects
                {
                    // 데이터 넣기
                };

                eventEffectList.Add(enemyEventEffect);
                break;
        }
    }

    return eventEffectList;
}

 

 

 

팩토리 패턴을 통해서 생성된 데이터 들은, 만약 랜덤 이벤트를 통해서 지속 효과가 추가될 경우, 아래에서 보이는 Clone()을 호출하여 프로토타입 패턴으로 생성된 복사본을 List에 넣어주는 것으로 추가했다.

 

 

 

public void AddEventEffect(int index)
{
    EventEffects effect = eventEffectList[index].Clone();
    switch (effect.duration)
    {
        case 0:
            effect.Apply();
            break;
        case 1:
            untillNextCombat.Add(effect);
            break;
        case 2:
            untillNextStage.Add(effect);
            break;
        case 3:
            untillEndAdventure.Add(effect);
            break;
    }
}

 

 


 

 

 

 

이렇게 등록된 효과들의 리스트는 간단히 Public으로 만들어진 메서드들에서. Apply()를 통해 효과를 적용가능하니, 저장이 가능한 List 형식을 가진 action과 같은 기능을 구현할 수 있었다. 실제로도 잘 적용이 되었고, 이와 같은 구조를 상황에 맞게 적절히 생각해 낸 경험을 글로 기록하고 싶었다.

 

 

 

사실은 처음 구조를 만들 때는 EventEffects 추상 클래스를 스크립터블 오브젝트 형식으로 만들어서 

ScriptableObject.CreateInstance <StatEventEffects>(); 를 통해 팩토리 패턴을 구현 및 등록해 주었으나, 기능적인 면에서는 문제없이 작동되며 저장도 가능하였다.

 

하지만 스크립터블 오브젝트들의 데이터는 유니티에서 직접 메모리를 관리하고 Destroy를 통해 연결을 끊어줘야 하는 번거로움이 존재하지만, 일반 클래스의 경우. net에서 메모리를 관리하며 참조가 끊기면 GC에서 자동으로 메모리 관리를 해 주기 때문에 최적화 적인 면을 고려하여 리팩토링을 진행한 것이다.

 

때문에 본문의 내용과 같이 스크립터블 오브젝트, 및 모노 비헤이비어를 받지 않는 일반 클래스 형식으로 효과들의 정보를 구현하게 되었다.