캐릭터 스탯 만들기

캐릭터 스탯 스크립트를 만들어준다.

캐릭터 스탯은 데이터 단위로만 사용해서 클래스로 객체화 시키지 않을 것이기 때문에 MonoBehaviour를 지워도 된다.

MonoBehaviour를 지웠기 때문에 유니티에서 사용할 수 있는 Start(), Update() 등의 메서드를 사용할 수 없다.

public enum StatsChangeType
{
    Add,
    Multiple,
    Override,
}

[Serializable]
public class CharacterStats
{
    public StatsChangeType statsChangeType;
    [Range(1, 100)] public int maxHealth;
    [Range(1f, 20f)] public float speed;

    // 공격 데이터 저장
}

공격 데이터를 저장할 때, 클래스를 기반으로 저장하면 각 객체마다 저장 공간을 가지게 되어 데이터 또한 똑같이 할당해주어야 하기 때문에 객체가 늘어나면 부담이 된다.

ScriptableObjects를 사용하면 하나의 데이터 컨테이너를 공유하여 사용할 수 있어 메모리적으로 간편하고, Inspector에서 바로 컨트롤 할 수 있어 접근적으로도 간편해진다.

 

유니티에서 ScriptableObjects 폴더와 스크립트를 따로 만들어준다.

 

ScriptableObject를 상속 받고 필요한 것들을 구현한다. 디폴트 공격값을 세팅한다.

Header()는 괄호 안의 내용이 Inspector에 뜨는 변수 위에 뜨게 한다.

[CreateAssetMenu(fileName ="DefaultAttackData", menuName ="TopDownController/Attacks/Default", order = 0)]

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

    [Header("Knock Back Info")]
    public bool isOnKnockback;
    public float knockbackPower;
    public float knockbackTime;
}

ScriptableObject를 사용하기 위해 데이터로 꺼내야하는데 메뉴로 추가시켜주면 된다.

[CreateAssetMenu(fileName ="처음 생성될 때 이름", menuName ="선택될 메뉴 이름", order = 0)]

 

원거리 공격을 메뉴에 추가하기 위해 RangedAttackData 스크립트를 만들어준다.

[CreateAssetMenu(fileName = "DefaultAttackData", menuName = "TopDownController/Attacks/Ranged", order = 1)]
public class RangedAttackData : AttackSO
{
    [Header("Ranged Attack Data")]
    public string bulletNameTag;
    public float duration;
    public float spread;
    public int numberofProjectilesPerShot;
    public float multipleProjectilesAngel;
    public Color projectileColor;
}

 

유니티로 돌아와서 Create에 확인해보면 메뉴가 생겨있다.

 

Datas폴더를 만들어서 플레이어의 공격을 추가하고 값을 설정해준다.

위에서 설정한 데이터는 공용이 되는 데이터로 한 플레이어의 값을 바꾸면 모든 플레이어의 값이 바뀐다.

 

캐릭터 스탯 스크립트에 공격 데이터를 추가해준다.

public enum StatsChangeType
{
    Add,
    Multiple,
    Override,
}

[Serializable]
public class CharacterStats
{
    public StatsChangeType statsChangeType;
    [Range(1, 100)] public int maxHealth;
    [Range(1f, 20f)] public float speed;

    // 공격 데이터 저장
    public AttackSO attackSO;
}

 

모든 플레이어의 값이 연동되지 않게 하기 위한 코드를 짠다.

baseStats.attackSO가 null이 아니라면 baseStats.attackSO를 자유롭게 수정하기 위해 가상으로 메모리상에 복제한다.

public class CharacterStatsHandler : MonoBehaviour
{
    // 수정자. 데이터가 추가로 들어오면 적용될 설정
    [SerializeField] private CharacterStats baseStats;
    public CharacterStats CurrentStates { get; private set; }
    public List<CharacterStats> statsModifiers = new List<CharacterStats>();

    private void Awake()
    {
        UpdateCharacterStats();
    }

    private void UpdateCharacterStats()
    {
        AttackSO attackSO = null;
        if (baseStats.attackSO != null)
        {
            attackSO = Instantiate(baseStats.attackSO);
        }

        CurrentStates = new CharacterStats { attackSO = attackSO };
        // TODO
        CurrentStates.statsChangeType = baseStats.statsChangeType;
        CurrentStates.maxHealth = baseStats.maxHealth;
        CurrentStates.speed = baseStats.speed;

    }
}

 

유니티에 돌아와서 플레이어의 스탯을 설정해준다.

 

 

TopDownMovement 스크립트로 돌아가서 스탯을 추가해서 GetComponent에 달아놓고 임의로 설정해둔 speed를 수정한다.

public class TopDownMovement : MonoBehaviour
{
    private TopDownCharactreController _controller;
    private CharacterStatsHandler _state; // 추가

    private Vector2 _movementDirection = Vector2.zero;
    private Rigidbody2D _rigidbody;

    private void Awake() 
    {
        _controller = GetComponent<TopDownCharactreController>();
        _state = GetComponent<CharacterStatsHandler>(); // 추가
        _rigidbody = GetComponent<Rigidbody2D>();
    }

    private void Start() 
    {
        _controller.OnMoveEvent += Move;
    }

    private void FixedUpdate() 
    {
        ApplyMovment(_movementDirection);
    }

    private void Move(Vector2 direction) 
    {
        _movementDirection = direction;
    }

    private void ApplyMovment(Vector2 direction)
    {
        direction = direction * _state.CurrentStates.speed; // 수정!
        _rigidbody.velocity = direction;
    }
}

 

TopDownCharactreController 역시 임의로 적어놓은 딜레이를 수정한다.

public class TopDownCharactreController : MonoBehaviour
{
    public event Action<Vector2> OnMoveEvent;
    public event Action<Vector2> OnLookEvent;
    public event Action OnAttackEvent;

    private float _timeSinceLastAttack = float.MaxValue;
    protected bool IsAttacking { get; set; }

    protected CharacterStatsHandler Stats { get; private set; } // 추가

    protected virtual void Awake()
    {
        Stats = GetComponent<CharacterStatsHandler>(); // 추가
    }

    protected virtual void Update() 
    {
        HandleAttackDelay();
    }

    private void HandleAttackDelay() 
    {
        // attack 정보가 없으면 공격하지 않는다
        if (Stats.CurrentStates.attackSO == null)
        {
            return;
        }

        if(_timeSinceLastAttack <= Stats.CurrentStates.attackSO.delay)  // 수정
        {
            _timeSinceLastAttack += Time.deltaTime;
        }

        if(IsAttacking && _timeSinceLastAttack > Stats.CurrentStates.attackSO.delay) // 수정
        {
            _timeSinceLastAttack = 0;
            CallAttackEvent();
        }
    }

    public void CallMoveEvent(Vector2 direction) 
    {
        OnMoveEvent?.Invoke(direction);
    }

    public void CallLookEvent(Vector2 direction) 
    {
        OnLookEvent?.Invoke(direction);
    }

    public void CallAttackEvent() 
    {
        OnAttackEvent?.Invoke();
    }
}

 

PlayerInputController 스크립트에서 Awake를 override 한다.

protected override void Awake() 
{
    base.Awake();
    _camera = Camera.main;
}

 

유니티로 돌아와보면 Inspector 창에서 Delay를 조절할 수 있게 됐다.

 

 

ScriptableObject를 만드는 것까지는 이해 됐는데 모든 플레이어의 스탯을 연동되지 않게 처리하는 부분에서 슬슬 이해가 안되기 시작했다... 주말에 이론과 특강을 포함해서 강의를 한 번 더 봐야할 것 같다.

+ Recent posts