ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TopDown Shooting Game / Stat System 만들기
    스파르타 게임 개발 2024. 5. 21. 01:28

    스텟을 작성 한다. 공격력, 이동속도, 등등을 담고 있는 클래스를 만들고, 그걸 기본스텟과 현재스텟으로 객체를 만들고 여러곳에서 활용 하게 하자. 

     

    사용한 방법은 먼저 ScriptablObject를 활용 해서 능력치를 담을 변수들을 만들어 주고, CharacterStat 클래스에서 입력을 받아 스텟을 담아주고 CharacterStatsHandler라는 클래스에서 객체들을 만들어서 여러 곳에서 활용 하게 만들어 준다.

     

    먼저 ScriptableObject는 클래스를 파일 형식으로 저장할 수 있게 해준다. 클래스를 ScriptableObject로 만들어 주면, 우리는 그 클래스를 Asset 형식으로 저장할 수 있다.

    이 클래스에는 여러 정보와 능력치를 담을 수 있으며, 공통된 작업을 해야 하는 오브젝트들이 있을 때 유용하다. 에셋 형식으로 클래스를 만들어 필드 값만 변경하고 오브젝트에 넣어주면 된다. 이러한 방식은 데이터 관리와 메모리 관리를 효율적으로 하고, 재사용성이 매우 뛰어난 강력한 기능이다. 일을 효율적으로 할 수 있게 해준다. 

     

    사용방법은 클래스를 파일형식으로 만들어 줘야 한다. 

     

    public class AttackSO : ScriptableObject

     

    AttackSO라는 걸 만들 거고 ScriptableObject를 상속 받는다. 그리고 CreateAssetMenu 를 사용 해야 한다.

     

    [CreateAssetMenu(fileName = "DefaultAttackSO", menuName = "TopDownController/Attacks/Default", order = 0)]
    public class AttackSO : ScriptableObject

     

    이렇게 클래스 위코드공간에 CreateAssetMenu를 사용해서 추가를 해주게 되면 우린 Asset을 생성 할 때 새로운 목록이 생긴걸 확인 할 수 있고 거기서 SO를 만들어서 활용을 할 수 있게 된다. 

    (order는 순서다./ DefalutAttackSo는 파일명인데 중요하지 않다. / TopDownController는 에셋을 추가 할 때 가장 먼저 나오게 되는 이름이다. 그리고 그 다음엔 Attack 카테고리 느낌, 그리고 Default는 해당 SO의 이름이 된다. )

     

    클래스를 만들어 줄 때 필드에 

     

    public class AttackSO : ScriptableObject
    {
        [Header("Attack Info")]
        public float size;
        public float delay;
        public float power;

     

    이러한 값들을 넣어주고 에셋을 추가해서 이 값들을 초기화 해줄 수 있게 된다. 그리고 이 정보가 필요한 오브젝트들에게 넣어주기만 하면 간단하게 일을 할 수 있게 되는 것이다. 

    (Header는 제목 같은 걸 붙여 주는 것이다. 사진참고)

    SO의 인스펙터, Attack Info라는 Header가 생긴 것이 보인다.

     

    이 SO를 등록 하는 오브젝트들은 이 SO의 값들을 사용 할 수 있게 되고 우리는 반복 되는 작업을 줄일 수 있다. 똑같은 능력치가 적용 되는 얘들에게 넣어주기만 하면 되는 것이다.

     

    지금은 AttackSO에서 "공격"이라는 공통 된 능력치가 담겨 있는 것을 만들고, 원거리공격이라는 RangedAttackSO라는 클래스를 만들고 AttackSO를 상속 받아 원거리공격 능력치만 추가 시켜주게 작업을 했다. 일을 효율적으로 하는 모습.

     

    이렇게 SO를 만들어서 다른 클래스에서 이 SO를 받아올 변수를 만들어 주고 추가 시켜주면 이 SO에 정보들에 접근해서 작업을 처리 할 수 있게 된다. 

     

    그래서 이제 CharacterStat이라는 클래스를 만들어서 능력치를 입력 받아올, 담아줄 클래스를 만들어 준다. 

    (객체 지향적으로 입력을 받는 클래스, 그 객체를 생성 해서 다른 곳에서 활용 하게 하는 클래스를 따로 만들어 준다.)

     

    [System.Serializable]
    public class CharacterStat
    {
        public StatsChangeType statsChangeType;
        [Range(1, 100)] public int maxHealth;
        [Range(1f, 20f)] public float speed;
        public AttackSO attackSO;
    }

     

    이렇게 클래스를 만들어 준다. 여기서 System.Serializabla은 클래스 전체를 직렬화를 해서 데이터를 관리 할 수 있게 하는 것이다. SerializaField는 한 개의 필드값만, Serializable은 클래스나, 구조체, 열거형 등을 직렬화 할 수 있게 해준다. 

    그래서 이 클래스에 있는 모든 것들을 Inspector에서 초기화 해줄 수 있게 된다. 

    여기서 StatsChangeType은 열거형인데 따로 적어두진 않겠다. 

     

    maxHealth와 speed에 적혀 있는 [Range(0, 0)]은 범위를 정해 주는 것이다. 인스펙터에서 슬라이드도 만들어줘서 마우스로 값을 초기화 해줄 수도 있다. 이러한 기능은 값을 넣기 편해지는 방법도 있지만 가독성도 좋게 해준다. 

    최소값과 최대값을 표현 해주니 최대로 얼만큼 넣어줘야 하나. 라는 것을 알 수 있게 해준다. 등등.

     

    AttackSO형식은 위와 같이 만들어둔 SO를 넣어주는 것만으로 그 값들을 다 가져올 수 있게 해주는 변수다.

    Character 클래스에 필드들을 인스펙터에서 값 넣어준 모습

    그런데 이번엔 이 CharacterStat 클래스를 사용 해서 값을 입력 받지 않는다. 우린 이 틀만 만들어 두고 CharacterStatsHandler라는 클래스를 사용 해서 객체를 만들고 이 클래스에 값을 넣어줄 것이다. 

     

    public class CharacterStatHandler : MonoBehaviour
    {
        [SerializeField] private CharacterStat baseStat;

     

    이렇게 클래스를 만들어 주면 baseStat이라는 CharacterStat클래스형 변수를 만들어 줄 수 있게 되고 인스펙터에서 값을 넣어줄 수 있게 된다. 

     

    그리고 우린 Base 스텟과 CurrentStat을 만들어 줘서 활용을 할 것이니 현재 스텟 변수도 만들어 준다. 

     

     public CharacterStat CurrentStat { get; private set; }

     

    여러 곳에서 현재스텟에 정보에 접근 할 것이니 Property 해준다. 세팅은 이 클래스에서만,

     

    이렇게 하면 baseStat은 인스펙터에서 값을 넣어주면 되지만 CurrentStat은 아직 비어있다. 이 스텟은 인스펙터에서 입력 받은 base스텟과 버프 등등을 활용 하여 적용 해줄 것이므로 이 점을 참고해 초기화 해주는 코드를 작성 한다. 

    클래스 안에서 객체를 생성 하게 하는 것도 해야 하므로 Awake를 먼저 사용 해준다(객체를 생성해야 CurrentStat을 초기화 할 수 있음)

     

        private void Awake()
        {
            UpdateCharacterStat();
        }

     

    라는 코드를 작성 해서 함수를 호출 하고 현재 스텟을 만드는 함수를 작성해준다. 

     

        private void UpdateCharacterStat()
        {
            AttackSO attackSO = null;
            if(baseStat.attackSO != null)
            {
                attackSO = Instantiate(baseStat.attackSO);
            }

            CurrentStat = new CharacterStat { attackSO = attackSO };

            CurrentStat.statsChangeType = baseStat.statsChangeType;
            CurrentStat.maxHealth = baseStat.maxHealth;
            CurrentStat.speed = baseStat.speed;
        }

     

    이렇게 코드를 작성 해주면 되는데 이 코드를 보게 되면 attackSO를 Instantiate를 사용해서 객체를 새로 생성 하는 것을 볼 수 있다. 밑에서 CurrentStat = new CharacterStat { attackSO = attackSO };를 통해 객체를 만드는데 굳이 또 왜 만드는가 싶을 수 있다. 이 이유는 SO는 참조형으로써 new를 통해 객체를 생성해도 동일한 인스턴스를 참조하게 되어 의도치 않은 데이터 변경이 일어날 수 있기 때문이다. 그렇기 때문에 Instantiate를 통해 새로 만들어 줘서 객체를 생성 할 때 새로 생성한 SO로 값을 바꿔주는 작업을 해줘야 한다. 객체를 만들 때 독립적인 인스턴스를 사용하도록 보장해야 한다.

    (if조건문을 사용한 이유는 attackSO는 null이 아니기 때문에 의미가 없기는 한데 보다 단단한 코드를 만들어 줄 수 있기 때문이라고 한다.)

     

    그리고 인스펙터로 초기화를 한 baseStat의 값들로 초기화를 해주면 현재스텟까지 만들어줄 수가 있게 된다. 

     

    이 방법을 통해 우리는 스탯 시스템을 객체지향적으로 설계하여, 유지보수가 용이하고 재사용성이 높으며 효율적인 작업을 가능하게 한다. 그리고 다른 객체들에서 GetComponent를 사용하여 스탯 정보를 가져와, 각 객체의 기능을 해당 스탯 정보에 맞게 구현할 수 있다. 특히, 부모 클래스에 한 번만 객체를 생성해두면, 자식 클래스에서는 별도의 설정 없이 동일한 스탯 시스템을 사용할 수 있다. 이로 인해 코드의 중복을 줄이고, 개발 및 유지보수의 효율성을 극대화할 수 있게 된다.

     

     

Designed by Tistory.