☸ 스텟과 중제자

HSD·2025년 3월 18일
0

팀 프로젝트

목록 보기
6/8

✤ 스텟 클래스

스텟을 구현하는 것은 크게 어렵지는 않았다.
하지만 이 스텟을 어떻게 관리할지가 제대로 판단되지않아 시간이 좀 걸렸다.

[Serializable]
public class Stat
{
    [SerializeField] private int baseValue;
    public List<int> modifiers;
    
    public int GetValue()
    {
        int totalValue = baseValue;

        foreach (int modifier in modifiers)
        {
            totalValue += modifier;
        }

        return totalValue;
    }

    public void AddModifier(int value) => modifiers.Add(value);

    public void RemoveModifier(int value) => modifiers.Remove(value);

    public void SetBaseValue(int value)
    {
        baseValue = value;
    }
}
  • Stat이란 클래스이고 스텟으로서 쓰일 예정이다.

  • List에 추가능력치를 추가할 수도있고 추가된 능력치를 볼 수도있으며

  • GetValue함수로 추가 능력치를 탐색하여 모두 합한 능력치를 가져올 수도있다.

✤ 스텟 시스템

스텟을 지정하고 관리하는 클래스이다.

public class CharacterStats : MonoBehaviour
{
    [Header("Offensive stat")]
    public Stat damage;
    public Stat critChance;
    public Stat critDamage;

    [Header("Defensive stat")]
    public Stat maxHealth;
    public Stat defense;

    [Header("Primary stat")]
    public Stat strength;
    public Stat agility;
    public Stat vitality;
    public Stat luck;

    public int currentHealth;

    public Action onHealthChanged;


    public void DoDamage(CharacterStats enemyStats, float attackPower = 1) => 
        StatsCalculate.CalculateTotalDamage(this, enemyStats,attackPower);

    private void Start()
    {
        currentHealth = StatsCalculator.StatsCalculate.GetMaxHealth(this);    
    }

    public void DecreaseHealth(int amount)
    {
        currentHealth -= amount;

        if(currentHealth <= 0)
        {
            Die();
        }

        if(onHealthChanged != null)
            onHealthChanged();
    }

    protected virtual void Die()
    {
        Debug.Log("죽었습니다!");
    }
}
  • 만들어둔 Stat클래스로 스텟들을 선언한다.
  • 그리고 DoDamage함수를 이용하여 데미지를 가할 상대의 스텟을
    매개변수로 넣으면 그 상대의 방어력 계산등이 이루어진 최종데미지가 가해진다.

✤ 중재자

객체와 객체의 의존성을 줄이고 함수 깊이가 깊어지기를 방지 하기위해 중재자를 두었다.

using UnityEngine;
using UnityEngine.TextCore.Text;

namespace StatsCalculator
{
    public static class StatsCalculate
    {
        static int strDamage     = 1;
        static int strMaxHealth  = 1;

        static int agiDamage     = 1;
        static int agiCritDamage = 1;
        static int agiCritChance = 1;

        static int vitMaxHealth  = 1;
        static int vitDefense    = 1;

        public static void CalculateTotalDamage(CharacterStats myStats, CharacterStats enemyStats, float attackPower)
        {
            float totalDamage;

            totalDamage = GetDamage(myStats) * attackPower;

            if(CanCrit(myStats))
            {
                totalDamage *= (GetCritDamage(myStats) / 100);
            }
            totalDamage = CheckTargetDefense(totalDamage, enemyStats);

            enemyStats.DecreaseHealth((int)totalDamage);
        }

        public static bool CanCrit(CharacterStats myStats)
        {
            if(GetCritChance(myStats) > Random.Range(0,100))
            {
                return true;
            }
            return false;
        }

        public static float CheckTargetDefense(float totalDamage,CharacterStats enemyStats)
        {
            totalDamage -= totalDamage * (GetDefense(enemyStats) / (GetDefense(enemyStats) + 50));

            return totalDamage;
        }

        #region Get Stats
        public static int GetDamage(CharacterStats Stats)
        {
            return Stats.damage.GetValue() +
                  (Stats.strength.GetValue() * strDamage + Stats.agility.GetValue() * agiDamage);
        }

        public static float GetDefense(CharacterStats stats)
        {
            return stats.defense.GetValue() + 
                   stats.vitality.GetValue() * vitDefense; 
        }

        public static int GetMaxHealth(CharacterStats stats)
        {
            return stats.maxHealth.GetValue() +
                   stats.vitality.GetValue() * vitMaxHealth + stats.strength.GetValue() * strMaxHealth;
        }

        public static int GetCritChance(CharacterStats stats)
        {
            return stats.critChance.GetValue() +
                   stats.agility.GetValue() * agiCritChance;
        }

        public static int GetCritDamage(CharacterStats stats)
        {
            return stats.critDamage.GetValue() +
                   stats.agility.GetValue() * agiCritDamage;
        }
        #endregion
    }

}

    
  • 객체와 객체가 서로 의존성을 가지는 것은 지금은 문제가 되지않지만
    나중에 다양한 시스템이 추가되고 그 시스템으로인해 서로의 소통 수가 늘어난다면 함수의 깊이는 더 깊어질
    것이고 매개변수도 더 많이 전달이 될 것이기에 불안정할 수 있다고 생각하여 중재자를 두었다.

  • 가해자가 중제자에게 피해자를 공격해줘라고 떠넘기면 중제자는 스텟에 대한 계산들을 끝낸 이 후
    피해자의 체력을 깎는 함수는 호출시킨다.

  • static Class이므로 계산식을 자유롭게 호출할 수 있는 것도 장점이다.

하지만

  • static으로 선언한 만큼 이 중재자는 내가 사용하지 않을때에도 어딘가에 존재하여 메모리 누수를 일으킨다.

  • 그리고 씬이 전환될때 만약 계산이 이루어지는 도중이었다면 가해자와 피해자의 정보를 그대로 가진채로
    씬이 전환 될 수 있기 때문에 완전히 안전하지는 않다.

  • 이 계산식은 아직 기본적인 계산식이라 무겁지는 않지만 나중에 반격, 패링, 반사 등 복잡한 식이 들어가게 되면
    이 중재자 자체가 무거워지게 되는 현상이 발생하고 이로인해 중재자에 의한 메모리 누수가 커질 수 있다.

그래서?

  • 제네릭 싱글톤으로 하여 관리하면 처음으로 가져올때 생성되기에 더 효율적이다.
    하지만 이 제네릭도 처음가져오면 쭉 유지되기에 이런 상황에서는 별로 효율이 좋지는 않다고 생각한다.

  • 그럼 남은건 내 생각엔 싱글톤중 WeakReference(약한참조) 싱글톤인데 바꿀 가능성도 있겠다.

✤ 마무리

현재 시스템은 설계단계로 언제든 바뀔 수 있지만 현상태의 동작은 잘 작동되는것으로 테스트 했기때문에
동작 자체의 변경은 크게 없을 것이고 추가를 한다던지 아니면 중재자를 더 효육적으로 사용하는 방법을 생각 해볼 수는 있겠다.

0개의 댓글