ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TIL] 10주차 4일 ( 옵저버패턴 , 캐릭터 스텟 강화 )
    개발일지/스파르타 코딩클럽 부트캠프 2024. 6. 20. 21:01

     

    오늘은 관찰자 패턴에 대해서 공부한 부분을 간단하게 볼건데 그 전에 자료초반부에도 써져있던 부분을 정리해봤다.

     

    모든 문제에 정답이란 없으며 확실치 않을 때는 Kiss(Keep it simple, stupid) 단순하게 유지하기, 수고를 더 들일 가치가 있을지 미리 생각하기 필요할때만 사용하기

    관찰자 패턴(Observer pattern)

    관찰자 패턴은 일대다 종속관계를 사용해 오브젝트가 통신하되 낮은 결합도를 유지하도록 할 수 있다. 한 오브젝트의 상태가 변경되면 종속된 모든 오브젝트가 자동으로 알림을 받는다.

     

    게임플레이 과정에서 발생하는 거의 모든 상황에 관찰자 패턴을 적용할 수 있다.

     

    관찰자 패턴의 장점

    • 결합도가 낮고 확장성이 높음
    • 런타임에서 옵저버 추가 제거가 용이
    • 유니티에선 구현이 용이

    단점

    • 시스템의 복잡성이 증가할 수 있고 디버깅이 힘든 경험이 생김
    • 관찰자들 사이에 호출순서가 예측 불가능함
    • 구독을 해지하지 않고 옵저버가 파괴될 경우 메모리 누수가 발생할 수 있음

     

    트러블슈팅


     

    팀프로젝트 과정 중에 아이템, 유물 등으로 플레이어의 스탯을 강화하는 부분에서 많은 어려움을 겪었다.

     

    일단 내가 고행을 겪었던 부분은 작성한 코드에서 생각과 다른 결과값이 도출되는 것이었는데

    먼저 작성한 CharacterStatHandler 스크립트를 보자

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    
    public class CharacterStatHandler : MonoBehaviour
    {
        [SerializeField] private CharacterStat baseStats;
    
        public CharacterStat CurrentStat { get; private set; } = new();
    
        public List<CharacterStat> statsModifiers = new List<CharacterStat>();
    
        private readonly int MinLv = 1;
        private readonly float MinAttackDmg = 1.0f;
        private readonly int MinMaxHealth = 1;
    
        private void Awake()
        {
            if (baseStats.statData != null)
            {
                baseStats.statData = Instantiate(baseStats.statData);
                CurrentStat.statData = Instantiate(baseStats.statData);
            }
            UpdateCharacterStat();
        }
    
    
        public void AddStatModifier(CharacterStat statModifier)
        {
            statsModifiers.Add(statModifier);
            UpdateCharacterStat();
        }
    
    
        public void RemoveStatModifier(CharacterStat statModifier)
        {
            statsModifiers.Remove(statModifier);
            UpdateCharacterStat();
        }
    
        private void UpdateCharacterStat()
        {
            ApplyStatModifier(baseStats);
    
            foreach (CharacterStat stat in statsModifiers.OrderBy(o => o.statsChangeType))
            {
                ApplyStatModifier(stat);
            }
        }
    
        private void ApplyStatModifier(CharacterStat modifier)
        {
            Func<float, float, float> operation = Operation(modifier.statsChangeType);
    
            UpdateAllStats(operation, modifier);
        }
    
        private Func<float, float, float> Operation(StatsChangeType statsChangeType)
        {
            return statsChangeType switch
            {
                StatsChangeType.Add => (current, change) => current + change,
                StatsChangeType.Multiple => (current, change) => current * change,
                StatsChangeType.Override => (current, change) => change
            };
        }
    
        private void UpdateAllStats(Func<float, float, float> operation, CharacterStat modifier)
        {
            if (CurrentStat.statData == null || modifier.statData == null) return;
    
            var currentStat = CurrentStat.statData;
            var newStat = modifier.statData;
    
            currentStat.Atk = Mathf.Max(operation(currentStat.Atk, newStat.Atk), MinAttackDmg);
            currentStat.MaxHealth = Mathf.Max((int)operation(currentStat.MaxHealth, newStat.MaxHealth), MinMaxHealth);
            currentStat.Lv = Mathf.Max((int)operation(currentStat.Lv, newStat.Lv), MinLv);
        }
    }

     

    베이스 스텟을 가져와서

    그 베이스 스텟을 기반으로 아이템 등 능력치가 추가될 때마다 새로운 연산을 하여 현재 능력치를 결과로 가져온다.

     

    public enum StatsChangeType
    {
        Add,
        Multiple,
        Override
    }
    
    [Serializable]
    public class CharacterStat
    {
        public StatsChangeType statsChangeType;
        public StatData statData;
    }

    CharacterStat 스크립트에서 enum을 가져와서 순서대로 작동시키게 했다.

    base에서 연산이 될 때 Add가 먼저, 그다음 Multiple, Override 순으로 진행된다.

    문제는 연산의 결과가 어디에 적용되는지 였는데

    내 예상은 데이터 컨테이너 자체가 변하는 줄 알았는데 그게 아니었다.

    public class Test : MonoBehaviour
    {
        
        [SerializeField] StatData playerData;
        public CharacterStatHandler characterStatHandler;
    
        void Start()
        {
            characterStatHandler = GetComponent<CharacterStatHandler>();
        }
    
        void Update()
        {
    
        }
    
        public void OnAttack()
        {
            Debug.Log($"{characterStatHandler.CurrentStat.statData.Atk}!!!");
        }
    }

    단순히 StatData가 바뀌는게 아닌 characterStatHandler.CurrentStat.statData가 바뀌는 것이기 때문에

    해당 부분을 찾아내고 스크립트를 수정했다.

    해당 부분에 맞춰서 ScriptableObject도 엄청 수정하고....

    코드에서 오류도 많이 생겨서 하루종일 디버깅만 함..

     

    결과를 찾아내면서 변경된 사항은 원래는 씬이 변경되도 데이터 컨테이너는 변하지 않으니까 조금 더 간단하게

    플레이어의 스텟을 유지할 수 있을 줄 알았는데 플레이어를 DontDestroy하여 유지시켜야 할 것 같다.

     

Designed by Tistory.