유니티2D 입문 정리 8 - 스텟 시스템 Base

woollim·2024년 11월 9일
0

0. 핵심내용

○ 열거형 (Enum)

  • 열거형은 상수형 값에 의미를 부여하기 위해 사용됩니다.
  • 기본 타입은 int기반으로, 제일 앞의 값을 0으로 하고 이후에 1, 2, 3,… 과 같이 번호를 붙입니다.
  • 중간에 번호 체계를 변경할 수도 있습니다. 중간에 데이터가 추가될 염려가 있는 경우에 이렇게 활용합니다(번호가 흐트러지면 데이터들이 다 망가질 수 있음)
    • 그때는 Weapon = 100 과 같이 값을 직접 넣어주게 됩니다. 이후에는 101, 102와 같이 그 다음 숫자가 배정됩니다.
    • 활용예 : if(ItemType == 0)일 때보다 if(ItemType == ItemType.Weapon)일 때가 훨씬 이해가 잘됩니다.
public enum ItemType
{
    Weapon,
    Armor,
    Consumable,
    QuestItem
}

○ 스크립터블 오브젝트 (Scriptable Object)

  • 스크립터블 오브젝트는 Unity에서 데이터를 저장하고 관리하는 유연한 데이터 컨테이너입니다.
  • 게임에서 재사용 가능한 데이터 또는 설정을 저장하는 데 사용됩니다.
  • 코드와 데이터를 분리하여 코드를 더 깔끔하고 관리하기 쉽게 만듭니다.
  • 하나의 스크립터블 오브젝트를 여러 게임 오브젝트에서 참조하거나 재사용할 수 있습니다.
  • Unity 에디터와 통합되어 인스펙터 창에서 직접 수정하고 관리할 수 있습니다.


1. 캐릭터 스텟 만들기

○ Attack SO 만들기

using UnityEngine;

[CreateAssetMenu(fileName = "DefaultAttackSO", menuName = "TopDownController/Attacks/Default", order = 0)]
public class AttackSO : ScriptableObject
{
		// 공격에 대한 기준 데이터를 유니티 에디터 상에서 편하게 관리할 수 있어요.
		// SO로 들고 있으면 모두가 이 SO를 바라보게 되어 중복된 데이터가 여기저기 흘러다니지 않는 장점이 있어요!
    [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;
}

○ RangedAttackSO 만들기

using UnityEngine;

[CreateAssetMenu(fileName = "RangedAttackSO", menuName = "TopDownController/Attacks/Ranged", order = 1)]
public class RangedAttackSO : AttackSO
{
    // 원거리 공격에만 있는 옵션 정의
    [Header("Ranged Attack Data")]
    public string bulletNameTag;
    public float duration;
    public float spread;
    public int numberofProjectilesPerShot;
    public float multipleProjectilesAngel;
    public Color projectileColor;
}

○ CharacterStat 만들기

using System;
using UnityEngine;

// Add 먼저하고, Multiple하고, 마지막에 Override하는 개념임
// enum에는 각각 정수가 매핑되어있기 때문에 가능 (0, 1, 2,...) 
// => 차후에 정렬 활용하면 오름차순으로 정렬활용해서 체계적으로 버프효과 적용순서 관리 가능
// Q: Override가 마지막에 있으면 무슨 효과가 있을까요?
// A: 공격력을 고정해야하는 특정 로직이나 기본 공격력 적용에 활용 가능
public enum StatsChangeType
{
	Add, // 0
	Multiple, // 1
	Override, // 2
}

// 클래스가 Serializable한 멤버로만 구성되어 있으면 [Serializable]을 붙여 에디터에서 확인 가능해요!
[Serializable]
public class CharacterStat
{
	public StatsChangeType statsChangeType;
	[Range(1, 100)] public int maxHealth; 
    // 인스펙터 창에서 maxHealth 변수를 1에서 100 사이의 값으로만 조정할 수 있게 함
	[Range(1f, 20f)] public float speed;
	public AttackSO attackSO;
}

○ CharacterStatHandler 만들기

using UnityEngine;
using System.Collections.Generic;

public class CharacterStatHandler : MonoBehaviour
{
    // 기본 스탯과 버프 스탯들의 능력치를 종합해서 스탯을 계산하는 컴포넌트
    [SerializeField] private CharacterStat baseStats;
    public CharacterStat CurrentStat { get; private set; }
    public List<CharacterStat> statsModifiers = new List<CharacterStat>();

    private void Awake()
    {
        UpdateCharacterStat();
    }

    private void UpdateCharacterStat()
    {
        // statModifier를 반영하기 위해 baseStat을 먼저 받아옴
        AttackSO attackSO = null;
        if (baseStats.attackSO != null)
        {
            attackSO = Instantiate(baseStats.attackSO);
        }

        CurrentStat = new CharacterStat { attackSO = attackSO };
        // TODO : 지금은 기본 능력치만 적용되고 있지만, 향후 능력치 강화 기능등이 추가될 것임!
        CurrentStat.statsChangeType = baseStats.statsChangeType;
        CurrentStat.maxHealth = baseStats.maxHealth;
        CurrentStat.speed = baseStats.speed;

    }
}

○ TopDownMovement 수정

using UnityEngine;

// TopDownMovement는 캐릭터와 몬스터의 이동에 사용될 예정입니다.
public class TopDownMovement : MonoBehaviour
{
    private TopDownController movementController;
    private CharacterStatHandler characterStatHandler;
    private Rigidbody2D movementRigidbody;

    private Vector2 _movementDirection = Vector2.zero;

    private void Awake()
    {
    // 같은 게임오브젝트의 TopDownController, Rigidbody를 가져올 것 
        movementController = GetComponent<TopDownController>();
        movementRigidbody = GetComponent<Rigidbody2D>();
        characterStatHandler = GetComponent<CharacterStatHandler>();
    }

    private void Start()
    {
        // OnMoveEvent에 Move를 호출하라고 등록함
        movementController.OnMoveEvent += Move;
    }

    private void FixedUpdate()
    {
        // 물리 업데이트에서 움직임 적용
        ApplyMovement(_movementDirection);
    }

    private void Move(Vector2 direction)
    {
        // 이동방향만 정해두고 실제로 움직이지는 않음.
        // 움직이는 것은 물리 업데이트에서 진행(rigidbody가 물리니까)
        _movementDirection = direction;
    }

    private void ApplyMovement(Vector2 direction)
    {
		    // 5라는 기본 스피드 숫자가 아니라 SO값을 참조하도록 변경했음
        direction = direction * characterStatHandler.CurrentStat.speed;

        movementRigidbody.velocity = direction;
    }
}

○ TopDownController 수정

using System;
using UnityEngine;

public class TopDownController : MonoBehaviour
{
    // Action은 void형 메소드를 등록할 수 있는데 Vector2를 인자로 받는 메소드를 등록하도록 Action<Vector2>를 정의
    // event 키워드를 입력하면 public이더라도 나만 Invoke가능.
    public event Action<Vector2> OnMoveEvent;
    public event Action<Vector2> OnLookEvent;
    
    // OnAttackEvent는 마우스 위치나 이동방향을 받지 않는 Action임
    public event Action OnAttackEvent;

    private float timeSinceLastAttack = float.MaxValue;
    protected bool isAttacking;

    protected CharacterStatHandler stats { get; private set; }

    protected virtual void Awake()
    {
        stats = GetComponent<CharacterStatHandler>();
    }

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

    private void HandleAttackDelay()
    {
        if (timeSinceLastAttack <= stats.CurrentStat.attackSO.delay)
        {
            timeSinceLastAttack += Time.deltaTime;
        }

        if (isAttacking && timeSinceLastAttack > stats.CurrentStat.attackSO.delay)
        {
            timeSinceLastAttack = 0;
            CallAttackEvent();
        }
    }

    public void CallMoveEvent(Vector2 direction)
    {
        // onMoveEvent는 public이어서 TopDownMovement에서 메소드들을 등록해놨음(a.k.a. 구독)
        OnMoveEvent?.Invoke(direction);
    }

    public void CallLookEvent(Vector2 direction)
    {
        // 조준 시스템에서 등록했음.
        OnLookEvent?.Invoke(direction);
    }

    public void CallAttackEvent()
    {
        // TopDownShooting에서 등록한 OnShoot 메소드가 구독되어 있음.
        OnAttackEvent?.Invoke();
    }
}

○ PlayerInputController 수정

// 부모 클래스에서 Awake가 virtual로 정의되어 있으니 그거에 얹어서! 추가적인 로직을 실행해야겠죠?
private void Awake()
protected override void Awake()
{
		// 부모의 Awake도 빼먹지 말고 실행하라는 의미
    base.Awake();
    _camera = Camera.main;
}

---- 생략 ----

○ SO 데이터 만들기

  • ScriptableObjects\Datas 폴더→ Ranged 데이터 생성
  • PlayerRangedAttack으로 이름 설정
  • 다음과 같이 데이터 설정
  • Character Stats Handler 설정
    • Character - CharacterStatsHandler 컴포넌트 추가
    • AttackSO → Player_RangedAttackData 설정

0개의 댓글