캐릭터나 오브젝트의 다양한 수치적 속성(생명력, 공격력 등)을 관리하는 Stat과 그 스탯들을 관리하는 Status를 구현한다.

 

Stat

구현 방법

새로운 스탯 인스턴스를 생성하여 초기값을 생성하는 생성자를 만들고, StatModifier에서 modifier의 값을 받아 스탯의 수정치를 확인하고 수정하는 함수를 구현한다.

 

 

구현 기능

Stat(): 새로운 스탯 인스턴스를 생성하고 초기값을 생성하는 생성자.
SetValue(): 값을 받으면 바뀐 값을 대입한다.
AddModifier() /RemoveModifier(): 수정치를 추가하거나 삭제한다.
GetModifyValue(): Value를 해당하는 StatModifierType으로 계산한다.

public class Stat
{
    public StatType Type { get; private set; }
    public float Min { get; private set; }
    public float Max { get; private set; }
    public float Value { get; private set; }
    public float OriginValue { get; private set; }

    private List<StatModifier> _modifiers = new(); // 스탯이 가지고 있는 수정치 목록.

    public event Action<Stat> OnChanged;

    // 새로운 스탯 인스턴스를 생성하고 초기값을 생성하는 생성자.
    public Stat(StatType type, float value = 0, float min = 0, float max = float.MaxValue)
    {
        this.Type = type;
        this.Min = min;
        this.Max = max;
        SetValue(value);
    }

    // 값을 받으면 바뀐 값을 대입.
    public void SetValue(float value)
    {
        OriginValue = value;
        Value = GetModifyValue();
        OnChanged?.Invoke(this);
    }

    // 수정치를 추가하거나 삭제하는 기능.
    public void AddModifier(StatModifier modifier)
    {
        _modifiers.Add(modifier);
        Value = GetModifyValue();
        OnChanged?.Invoke(this); // 능력치에 변화가 있다면 호출.
    }
    public void RemoveModifier(StatModifier modifier)
    {
        _modifiers.Remove(modifier);
        Value = GetModifyValue();
        OnChanged?.Invoke(this); // 능력치에 변화가 있다면 호출.
    }


    // _modifiers 리스트에 들어있는 객체들의 StatModifierType을 비교하여 Value를 해당하는 StatModifierType으로 계산한다.
    private float GetModifyValue()
    {
        float value = OriginValue;
        for (int i = 0; i < _modifiers.Count; i++)
        {
            // Stat 계산 방법.
            if (_modifiers[i].Type == StatModifierType.Add) value += _modifiers[i].Value;
            else if (_modifiers[i].Type == StatModifierType.Multiple) value *= _modifiers[i].Value;
            else if (_modifiers[i].Type == StatModifierType.Override) value = _modifiers[i].Value;
        }
        value = Mathf.Clamp(value, Min, Max);
        return value;
    }
}

 

 

Status

구현 방법

수정할 스탯 리스트를 받아 알맞은 스탯에 추가하거나 제거하는 기능을 구현한다.

 

 

구현 기능

StatType: 스탯 타입들. COUNT는 갯수를 파악하기 위한 상수이다.

StatModifierType: 수정치 계산 종류.

Stat this: 스탯 종류를 받아온다.

Status(): 스탯 종류를 확인 후, 딕셔너리에 추가.

Status(CreatureData data): 기존 데이터를 받아와 스탯에 대입한다.

AddModifiers / RemoveModifiers: 변화될 스탯을 리스트에서 찾아 스탯에 변화를 주는 함수들를 추가하거나 제거한다.

public enum StatType
{
    HpMax,
    HpRegen,
    Damage,
    Defense,
    MoveSpeed,
    AttackSpeed,
    //Cost,
    //Range,
    //Sight,
    COUNT // StatType 갯수 파악을 위한 상수.
}

public enum StatModifierType
{
    Add,
    Multiple,
    Override,
}


public class Status
{
    private Dictionary<StatType, Stat> _stats;

    public Stat this[StatType type]
    {
        get => _stats[type];
    }

    // 스탯 타입을 확인하고 딕셔너리에 추가.
    public Status()
    {
        _stats = new();
        for (int i = 0; i < (int)StatType.COUNT; i++)
        {
            _stats.Add((StatType)i, new Stat((StatType)i));
        }
    }

    // 크리처의 기존 데이터를 받아와서 스탯에 대입.
    public Status(CreatureData data)
    {
        _stats = new()
        {
            [StatType.HpMax] = new(StatType.HpMax, data.HpMax),
            [StatType.HpRegen] = new(StatType.HpRegen, data.HpRegen),
            [StatType.Damage] = new(StatType.Damage, data.Damage),
            [StatType.Defense] = new(StatType.Defense, data.Defense),
            [StatType.MoveSpeed] = new(StatType.MoveSpeed, data.MoveSpeed),
            [StatType.AttackSpeed] = new(StatType.AttackSpeed, data.AttackSpeed),
        };
    }

    // 변화될 스탯을 리스트에서 찾아 스탯에 변화를 주는 함수를 추가 / 제거.
    public void AddModifiers(List<StatModifier> modifiers)
    {
        for (int i = 0; i < modifiers.Count; i++)
        {
            this[modifiers[i].Stat].AddModifier(modifiers[i]);
        }
    }
    public void RemoveModifiers(List<StatModifier> modifiers)
    {
        for (int i = 0; i < modifiers.Count; i++)
        {
            this[modifiers[i].Stat].RemoveModifier(modifiers[i]);
        }
    }
}

'부트캠프 > Project' 카테고리의 다른 글

Skill 구상  (0) 2024.01.23
Projectile 구현  (0) 2024.01.19
StatModifier 구현 계획  (0) 2024.01.17
Resource Manager 구현  (0) 2024.01.15
<프로젝트> 팀과제 Unity 리듬 게임 회고  (2) 2024.01.09

스탯을 각 개체 클래스의 필드로 직접 관리하게 되면, 다양한 유형의 수정치를 적용하거나 스탯 간 복잡한 상호작용을 구현하기 어렵고 스탯 관리 로직이 특정 클래스와 밀접하게 결합되어 있으면 다른 클래스나 시스템에서 재사용하기 어렵다.

새로운 스탯 타입 또는 수정치를 추가하기 위해 기존 클래스를 크게 수정해야 할 수도 있어 유지보수가 어렵고 비슷한 스탯 관리 코드가 중복될 가능성도 있다.

그렇기 때문에 기존 스탯과 결합하여 스탯을 변화시키는 수정치인 StatModifier를 따로 구현하기로 했다.

 

구현 계획

  • 어떤 스탯을 얼마나 어떻게 수정할 것인지 정보를 받아온다.
  • 새로운 수정치 인스턴스를 생성하고 초기값을 설정하는 생성자
  • string 값(Modifiers)을 받아 새 인스턴스를 생성하는 생성자

 

Stat에서 캐릭터나 오브젝트의 다양한 수치적 속성(생명력, 공격력 등)을 관리하고 Status에서 모든 Stat을 관리하도록 Class를 분리한다.

public class StatModifier
{
    public StatType Stat { get; set; }
    public StatModifierType Type { get; set; }
    public float Value { get; set; }

    // 기본 생성자.
    public StatModifier() { }

    // Modifier 값을 받아온다.
    public StatModifier(StatType stat, StatModifierType type, float value)
    {
        Stat = stat;
        Type = type;
        Value = value;
    }

    // json 파일의 Modifier를 나눠서 Enum으로 변환한다. 
    public StatModifier(string s)
    {
        string[] strings = s.Split('_');
        Stat = (StatType)Enum.Parse(typeof(StatType), strings[0]);
        Type = (StatModifierType)Enum.Parse(typeof(StatModifierType), strings[1]);
        Value = float.Parse(strings[2]);
    }

    // 받은 값들을 깊은복사한다.
    public StatModifier Copy()
    {
        return new(Stat, Type, Value);
    }
}

 

'부트캠프 > Project' 카테고리의 다른 글

Projectile 구현  (0) 2024.01.19
Stat / Status 구현  (0) 2024.01.18
Resource Manager 구현  (0) 2024.01.15
<프로젝트> 팀과제 Unity 리듬 게임 회고  (2) 2024.01.09
<프로젝트> 팀과제 Unity 리듬 게임(4)  (1) 2024.01.05

Prefab 하나로 Data에 따라 Sprite나 Animation 등을 바꿔주며 다른 Object가 되도록 설정할 수 있도록 구현하기 위해 Data와 Resource를 구분하였다. 프로젝트에서 내가 맡은 부분은 Resource로 리소스를 불러오고 외부에서 리소스를 사용하라 수 있도록 했다.

 

구현 방법

소규모 프로젝트에서는 Resources.Load 방식으로 구현하는 것이 더 간단하기 때문에 모든 리소스를 Resources 폴더에 넣고 Resources.Load 또는 Resources.LoadAll 함수를 통해 리소스를 불러오는 방식을 택하였다. 

 

구현 기능

게임 시작 시 게임에 Resource 경로에서 필요한 리소스(Sprite, Prefab, JsonData, Animation)를 불러온다.

Initialize( ): 리소스를 불러올 경로를 지정하여 Dictionary에 추가한다. 현재 Sprites의 경로가 지정되어 있지 않기 때문에 후에 수정해야 한다.

Load~( ): Dictionary의 key값으로 리소스가 있는지 확인 후, 리턴한다.

Instantiate( ): 오브젝트가 풀 안에 있는지 확인 후, parent 하위에 prefab을 생성하거나 내보낸다.

Destroy( ): 필요 없는 오브젝트를 파괴한다.

public class ResourceManager : MonoBehaviour
{
    private Dictionary<string, Sprite> _sprites = new();
    private Dictionary<string, GameObject> _prefabs = new();
    private Dictionary<string, TextAsset> _jsonData = new();
    private Dictionary<string, RuntimeAnimatorController> _animControllers = new();

    public void Initialize()
    {
        Sprite[] sprites = Resources.LoadAll<Sprite>("Sprites/"); // TODO: 경로 지정.
        foreach (Sprite sprite in sprites)
        {
            _sprites.Add(sprite.name, sprite);
        }

        GameObject[] objs = Resources.LoadAll<GameObject>("Prefabs");
        foreach (GameObject obj in objs)
        {
            _prefabs.Add(obj.name, obj);
        }

        TextAsset[] texts = Resources.LoadAll<TextAsset>("JsonData");
        foreach (TextAsset t in texts)
        {
            _jsonData.Add(t.name, t);
        }

        RuntimeAnimatorController[] controllers = Resources.LoadAll<RuntimeAnimatorController>("Animations");
        foreach (RuntimeAnimatorController controller in controllers)
        {
            _animControllers.Add(controller.name, controller);
        }
    }

    // 리소스가 있는지 확인.
    public GameObject LoadPrefab(string key)
    {
        if (!_prefabs.TryGetValue(key, out GameObject prefab))
        {
            Debug.LogError($"[ResourceManager] LoadPrefab({key}): Failed to load prefab.");
            return null;
        }
        return prefab;
    }
    public Sprite LoadSprite(string key)
    {
        if (!_sprites.TryGetValue(key, out Sprite sprite))
        {
            Debug.LogError($"[ResourceManager] LoadSprite({key}): Failed to load sprite.");
            return null;
        }
        return sprite;
    }
    public TextAsset LoadJsonData(string key)
    {
        if (!_jsonData.TryGetValue(key, out TextAsset data))
        {
            Debug.LogError($"[ResourceManager] LoadJsonData({key}): Failed to load jsonData.");
            return null;
        }
        return data;
    }
    public RuntimeAnimatorController LoadAnimController(string key)
    {
        if (!_animControllers.TryGetValue(key, out RuntimeAnimatorController controller))
        {
            Debug.LogError($"[ResourceManager] LoadJsonData({key}): Failed to load animController.");
            return null;
        }
        return controller;
    }

    // 오브젝트가 풀 안에 있는지 없는지 확인 후 생성.
    public GameObject Instantiate(string key, Transform parent = null, bool pooling = false)
    {
        GameObject prefab = LoadPrefab(key);
        if (prefab == null)
        {
            Debug.LogError($"[ResourceManager] Instantiate({key}): Failed to load prefab.");
            return null;
        }

        if (pooling) return Main.Pool.Pop(prefab);

        GameObject obj = GameObject.Instantiate(prefab, parent);
        obj.name = prefab.name;
        return obj;
    }

    // 필요없는 오브젝트 파괴.
    public void Destroy(GameObject obj)
    {
        if (obj == null) return;

        if (Main.Pool.Push(obj)) return;

        Object.Destroy(obj);
    }
}

 

 

팀프로젝트가 마무리 되었다. 발표를 마치고 아쉬웠던 점을 팀원분들과 이야기했다.

일단 어디서 잘못 건든건지 리듬게임에 가장 치명적인 싱크가 안맞는 문제가 있었다는게 아쉬웠다. 노트 생성이 게임이 시작하자마자 되도록 바뀌어 있어서 노트와 노트 타격하는 곳의 거리 계산이 적용되고 있지 않는데 아마 노트 클론을 리스트로 옮기면서 생긴 버그 같다.

그 외에도 씬 전환 시, 아이템 장착이나 설정 옵션이 뜨지 않는 등의 문제가 있었다.

실력이 부족하여 구현하지 못한 아쉬움도 많았지만  평생 꼭 도전해보고 싶었던 리듬게임이라는 장르를 도전하여 제작한 과정이 진심으로 즐거웠다. 나중에 최종 프로젝트가 끝나고 개인 과제를 좀더 다듬어서 다시 도전해보고 싶다.

+ Recent posts